djbob2000

Untitled

Sep 1st, 2025
240
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import { AST_NODE_TYPES, ESLintUtils, type TSESTree, type TSESLint } from "@typescript-eslint/utils";
  2. import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
  3.  
  4. const defaultMinLines = 3;
  5.  
  6. const constants = {
  7.     url: "https://example.com/rules/",
  8.     ruleName: "must-avoid-code-duplication-in-functions",
  9.     messageIds: {
  10.         mustAvoidCodeDuplicationInFunctions: "mustAvoidCodeDuplicationInFunctions",
  11.     },
  12.     meta: {
  13.         type: "problem",
  14.         docs: {
  15.             description: "Avoid duplicate code blocks in different functions.",
  16.         },
  17.         // Approved
  18.         /* eslint-disable max-len */
  19.         messages: {
  20.             mustAvoidCodeDuplicationInFunctions: `must-avoid-code-duplication-in-functions
  21. This function is identical to the function on line {{line}}.
  22.  
  23. ### **Must** avoid code duplication in functions.
  24.  
  25. Identical or nearly identical code blocks were found in different functions. Duplicated logic makes maintenance harder and increases the risk of bugs. Extract shared logic into a reusable function.
  26.  
  27. #### Wrong:
  28.  
  29. function foo() {
  30.     const baz = 1;
  31.     const qux = 2;
  32.     return baz + qux;
  33. }
  34.  
  35. function bar() {
  36.     const baz = 1;
  37.     const qux = 2;
  38.     return baz + qux;
  39. }
  40.  
  41. #### Correct:
  42.  
  43. function sum() {
  44.     const baz = 1;
  45.     const qux = 2;
  46.     return baz + qux;
  47. }
  48.  
  49. function foo() {
  50.     return sum();
  51. }
  52.  
  53. function bar() {
  54.     return sum();
  55. }
  56.  
  57. #### Why:
  58.  
  59. Eliminating duplicate code improves maintainability, makes refactoring easier, and reduces the chances of introducing errors.`,
  60.         },
  61.         /* eslint-enable max-len */
  62.         schema: [
  63.             {
  64.                 type: "object",
  65.                 properties: {
  66.                     minLines: {
  67.                         type: "integer",
  68.                         minimum: 1,
  69.                     },
  70.                 },
  71.                 additionalProperties: false,
  72.             },
  73.         ],
  74.     },
  75. } as const;
  76.  
  77. const tokens = {
  78.     functionToken: "function",
  79.     arrowToken: "=>",
  80.     openBrace: "{",
  81.     closeBrace: "}",
  82. } as const;
  83.  
  84. type MessageIds = keyof typeof constants.messageIds;
  85. type Options = {
  86.     minLines: number;
  87. }[];
  88. type FunctionNode =
  89.     | TSESTree.FunctionDeclaration
  90.     | TSESTree.FunctionExpression
  91.     | TSESTree.ArrowFunctionExpression;
  92. type Functions = { function: FunctionNode; parent?: TSESTree.Node }[];
  93.  
  94. function areEquivalent(
  95.     first: TSESTree.Node | TSESTree.Node[],
  96.     second: TSESTree.Node | TSESTree.Node[],
  97.     sourceCode: TSESLint.SourceCode,
  98. ): boolean {
  99.     if (Array.isArray(first) && Array.isArray(second)) {
  100.         return (
  101.             first.length === second.length &&
  102.             first.every((firstNode, index) => {
  103.                 const secondNode = second[index];
  104.  
  105.                 if (!secondNode) return false;
  106.  
  107.                 return areEquivalent(firstNode, secondNode, sourceCode);
  108.             })
  109.         );
  110.     } else if (!Array.isArray(first) && !Array.isArray(second)) {
  111.         return (
  112.             first.type === second.type &&
  113.             compareTokens(sourceCode.getTokens(first), sourceCode.getTokens(second))
  114.         );
  115.     }
  116.  
  117.     return false;
  118. }
  119.  
  120. function compareTokens(firstTokens: TSESLint.AST.Token[], secondTokens: TSESLint.AST.Token[]) {
  121.     return (
  122.         firstTokens.length === secondTokens.length &&
  123.         firstTokens.every((firstToken, index) => firstToken.value === secondTokens[index]?.value)
  124.     );
  125. }
  126.  
  127. function getMainFunctionTokenLocation(
  128.     sourceCode: TSESLint.SourceCode,
  129.     fn: TSESTree.FunctionLike,
  130.     parent?: TSESTree.Node,
  131. ) {
  132.     let location: TSESTree.SourceLocation | null | undefined;
  133.  
  134.     if (fn.type === AST_NODE_TYPES.FunctionDeclaration) {
  135.         if (fn.id) {
  136.             location = fn.id.loc;
  137.         } else {
  138.             const token = getTokenByValue(fn, tokens.functionToken, sourceCode);
  139.             location = token?.loc;
  140.         }
  141.     } else if (fn.type === AST_NODE_TYPES.FunctionExpression) {
  142.         if (
  143.             parent &&
  144.             (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.Property)
  145.         ) {
  146.             location = parent.key.loc;
  147.         } else {
  148.             const token = getTokenByValue(fn, tokens.functionToken, sourceCode);
  149.             location = token?.loc;
  150.         }
  151.     } else if (fn.type === AST_NODE_TYPES.ArrowFunctionExpression) {
  152.         const token = sourceCode
  153.             .getTokensBefore(fn.body)
  154.             .reverse()
  155.             .find((tkn) => tkn.value === tokens.arrowToken);
  156.  
  157.         location = token?.loc;
  158.     }
  159.  
  160.     return location!;
  161. }
  162.  
  163. function getTokenByValue(node: TSESTree.Node, value: string, sourceCode: TSESLint.SourceCode) {
  164.     return sourceCode.getTokens(node).find((token) => token.value === value);
  165. }
  166.  
  167. function processFunctions(context: RuleContext<MessageIds, Options>, functions: Functions) {
  168.     const functionGroups = groupFunctionsByTokens(context.sourceCode, functions);
  169.     reportDuplicatesInGroups(context, functionGroups);
  170. }
  171.  
  172. function groupFunctionsByTokens(
  173.     sourceCode: TSESLint.SourceCode,
  174.     functions: Functions,
  175. ) {
  176.     const functionGroups = new Map<string, { function: FunctionNode; parent?: TSESTree.Node; line: number }[]>();
  177.  
  178.     for (const entry of functions) {
  179.         if (entry.function.body) {
  180.             const bodyTokens = sourceCode.getTokens(entry.function.body);
  181.             const tokenKey = bodyTokens.map(token => token.value).join("");
  182.            
  183.             const functionInfo = {
  184.                 function: entry.function,
  185.                 parent: entry.parent,
  186.                 line: entry.function.loc?.start.line ?? 0,
  187.             };
  188.  
  189.             const existingGroup = functionGroups.get(tokenKey);
  190.  
  191.             if (existingGroup) {
  192.                 existingGroup.push(functionInfo);
  193.             } else {
  194.                 functionGroups.set(tokenKey, [functionInfo]);
  195.             }
  196.         }
  197.     }
  198.  
  199.     return functionGroups;
  200. }
  201.  
  202. function reportDuplicatesInGroups(
  203.     context: RuleContext<MessageIds, Options>,
  204.     functionGroups: Map<string, { function: FunctionNode; parent?: TSESTree.Node; line: number }[]>,
  205. ) {
  206.     const groups = Array.from(functionGroups.values());
  207.  
  208.     for (const group of groups) {
  209.         if (group.length >= 2) {
  210.             group.sort((a, b) => a.line - b.line);
  211.             const originalFunction = group[0];
  212.  
  213.             if (originalFunction?.function.loc) {
  214.                  reportDuplicatesInGroup(context, group, originalFunction);
  215.              }
  216.         }
  217.     }
  218. }
  219.  
  220. function reportDuplicatesInGroup(
  221.     context: RuleContext<MessageIds, Options>,
  222.     group: { function: FunctionNode; parent?: TSESTree.Node; line: number }[],
  223.     originalFunction: { function: FunctionNode; parent?: TSESTree.Node; line: number },
  224. ) {
  225.     for (let i = 1; i < group.length; i++) {
  226.         const duplicatingFunction = group[i];
  227.  
  228.         if (duplicatingFunction && areEquivalent(
  229.             duplicatingFunction.function.body,
  230.             originalFunction.function.body,
  231.             context.sourceCode
  232.         )) {
  233.             const loc = getMainFunctionTokenLocation(
  234.                 context.sourceCode,
  235.                 duplicatingFunction.function,
  236.                 duplicatingFunction.parent,
  237.             );
  238.  
  239.             context.report({
  240.                 messageId: constants.messageIds.mustAvoidCodeDuplicationInFunctions,
  241.                 data: {
  242.                     line: originalFunction.function.loc!.start.line,
  243.                 },
  244.                 loc,
  245.             });
  246.         }
  247.     }
  248. }
  249.  
  250. function visitFunction(
  251.     sourceCode: TSESLint.SourceCode,
  252.     node: FunctionNode,
  253.     functions: Functions,
  254.     minLines: number,
  255. ) {
  256.     let isBigEnough = false;
  257.     const srcTokens = sourceCode.getTokens(node.body);
  258.  
  259.     if (srcTokens.length > 0 && srcTokens[0]?.value === tokens.openBrace) {
  260.         srcTokens.shift();
  261.     }
  262.  
  263.     if (srcTokens.length > 0 && srcTokens[srcTokens.length - 1]?.value === tokens.closeBrace) {
  264.         srcTokens.pop();
  265.     }
  266.  
  267.     if (srcTokens.length > 0) {
  268.         const firstLine = srcTokens[0]?.loc.start.line;
  269.         const lastLine = srcTokens[srcTokens.length - 1]?.loc.end.line;
  270.  
  271.         if (lastLine && firstLine) {
  272.             isBigEnough = lastLine - firstLine + 1 >= minLines;
  273.         }
  274.     }
  275.  
  276.     if (isBigEnough) {
  277.         functions.push({ function: node, parent: node.parent });
  278.     }
  279. }
  280.  
  281. const createRule = ESLintUtils.RuleCreator((ruleName) => `${constants.url}${ruleName}`);
  282.  
  283. const mustAvoidCodeDuplicationInFunctions = createRule<Options, MessageIds>({
  284.     name: constants.ruleName,
  285.     meta: constants.meta,
  286.     defaultOptions: [{ minLines: defaultMinLines }],
  287.  
  288.     create(context) {
  289.         const functions: Functions = [];
  290.         const minLines = context.options[0]?.minLines ?? defaultMinLines;
  291.  
  292.         return {
  293.             FunctionDeclaration(node) {
  294.                 visitFunction(context.sourceCode, node, functions, minLines);
  295.             },
  296.  
  297.             "VariableDeclarator > FunctionExpression, MethodDefinition > FunctionExpression": (
  298.                 node: TSESTree.FunctionExpression,
  299.             ) => {
  300.                 visitFunction(context.sourceCode, node, functions, minLines);
  301.             },
  302.  
  303.             "VariableDeclarator > ArrowFunctionExpression, MethodDefinition > ArrowFunctionExpression": (
  304.                 node: TSESTree.ArrowFunctionExpression,
  305.             ) => {
  306.                 visitFunction(context.sourceCode, node, functions, minLines);
  307.             },
  308.  
  309.             "Property > FunctionExpression, Property > ArrowFunctionExpression": (
  310.                 node: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,
  311.             ) => {
  312.                 visitFunction(context.sourceCode, node, functions, minLines);
  313.             },
  314.  
  315.             "Program:exit"() {
  316.                 processFunctions(context, functions);
  317.             },
  318.         };
  319.     },
  320. });
  321.  
  322. export default mustAvoidCodeDuplicationInFunctions;
  323.  
Advertisement
Add Comment
Please, Sign In to add comment