Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import { AST_NODE_TYPES, ESLintUtils, type TSESTree, type TSESLint } from "@typescript-eslint/utils";
- import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
- const defaultMinLines = 3;
- const constants = {
- url: "https://example.com/rules/",
- ruleName: "must-avoid-code-duplication-in-functions",
- messageIds: {
- mustAvoidCodeDuplicationInFunctions: "mustAvoidCodeDuplicationInFunctions",
- },
- meta: {
- type: "problem",
- docs: {
- description: "Avoid duplicate code blocks in different functions.",
- },
- // Approved
- /* eslint-disable max-len */
- messages: {
- mustAvoidCodeDuplicationInFunctions: `must-avoid-code-duplication-in-functions
- This function is identical to the function on line {{line}}.
- ### **Must** avoid code duplication in functions.
- 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.
- #### Wrong:
- function foo() {
- const baz = 1;
- const qux = 2;
- return baz + qux;
- }
- function bar() {
- const baz = 1;
- const qux = 2;
- return baz + qux;
- }
- #### Correct:
- function sum() {
- const baz = 1;
- const qux = 2;
- return baz + qux;
- }
- function foo() {
- return sum();
- }
- function bar() {
- return sum();
- }
- #### Why:
- Eliminating duplicate code improves maintainability, makes refactoring easier, and reduces the chances of introducing errors.`,
- },
- /* eslint-enable max-len */
- schema: [
- {
- type: "object",
- properties: {
- minLines: {
- type: "integer",
- minimum: 1,
- },
- },
- additionalProperties: false,
- },
- ],
- },
- } as const;
- const tokens = {
- functionToken: "function",
- arrowToken: "=>",
- openBrace: "{",
- closeBrace: "}",
- } as const;
- type MessageIds = keyof typeof constants.messageIds;
- type Options = {
- minLines: number;
- }[];
- type FunctionNode =
- | TSESTree.FunctionDeclaration
- | TSESTree.FunctionExpression
- | TSESTree.ArrowFunctionExpression;
- type Functions = { function: FunctionNode; parent?: TSESTree.Node }[];
- function areEquivalent(
- first: TSESTree.Node | TSESTree.Node[],
- second: TSESTree.Node | TSESTree.Node[],
- sourceCode: TSESLint.SourceCode,
- ): boolean {
- if (Array.isArray(first) && Array.isArray(second)) {
- return (
- first.length === second.length &&
- first.every((firstNode, index) => {
- const secondNode = second[index];
- if (!secondNode) return false;
- return areEquivalent(firstNode, secondNode, sourceCode);
- })
- );
- } else if (!Array.isArray(first) && !Array.isArray(second)) {
- return (
- first.type === second.type &&
- compareTokens(sourceCode.getTokens(first), sourceCode.getTokens(second))
- );
- }
- return false;
- }
- function compareTokens(firstTokens: TSESLint.AST.Token[], secondTokens: TSESLint.AST.Token[]) {
- return (
- firstTokens.length === secondTokens.length &&
- firstTokens.every((firstToken, index) => firstToken.value === secondTokens[index]?.value)
- );
- }
- function getMainFunctionTokenLocation(
- sourceCode: TSESLint.SourceCode,
- fn: TSESTree.FunctionLike,
- parent?: TSESTree.Node,
- ) {
- let location: TSESTree.SourceLocation | null | undefined;
- if (fn.type === AST_NODE_TYPES.FunctionDeclaration) {
- if (fn.id) {
- location = fn.id.loc;
- } else {
- const token = getTokenByValue(fn, tokens.functionToken, sourceCode);
- location = token?.loc;
- }
- } else if (fn.type === AST_NODE_TYPES.FunctionExpression) {
- if (
- parent &&
- (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.Property)
- ) {
- location = parent.key.loc;
- } else {
- const token = getTokenByValue(fn, tokens.functionToken, sourceCode);
- location = token?.loc;
- }
- } else if (fn.type === AST_NODE_TYPES.ArrowFunctionExpression) {
- const token = sourceCode
- .getTokensBefore(fn.body)
- .reverse()
- .find((tkn) => tkn.value === tokens.arrowToken);
- location = token?.loc;
- }
- return location!;
- }
- function getTokenByValue(node: TSESTree.Node, value: string, sourceCode: TSESLint.SourceCode) {
- return sourceCode.getTokens(node).find((token) => token.value === value);
- }
- function processFunctions(context: RuleContext<MessageIds, Options>, functions: Functions) {
- const functionGroups = groupFunctionsByTokens(context.sourceCode, functions);
- reportDuplicatesInGroups(context, functionGroups);
- }
- function groupFunctionsByTokens(
- sourceCode: TSESLint.SourceCode,
- functions: Functions,
- ) {
- const functionGroups = new Map<string, { function: FunctionNode; parent?: TSESTree.Node; line: number }[]>();
- for (const entry of functions) {
- if (entry.function.body) {
- const bodyTokens = sourceCode.getTokens(entry.function.body);
- const tokenKey = bodyTokens.map(token => token.value).join("");
- const functionInfo = {
- function: entry.function,
- parent: entry.parent,
- line: entry.function.loc?.start.line ?? 0,
- };
- const existingGroup = functionGroups.get(tokenKey);
- if (existingGroup) {
- existingGroup.push(functionInfo);
- } else {
- functionGroups.set(tokenKey, [functionInfo]);
- }
- }
- }
- return functionGroups;
- }
- function reportDuplicatesInGroups(
- context: RuleContext<MessageIds, Options>,
- functionGroups: Map<string, { function: FunctionNode; parent?: TSESTree.Node; line: number }[]>,
- ) {
- const groups = Array.from(functionGroups.values());
- for (const group of groups) {
- if (group.length >= 2) {
- group.sort((a, b) => a.line - b.line);
- const originalFunction = group[0];
- if (originalFunction?.function.loc) {
- reportDuplicatesInGroup(context, group, originalFunction);
- }
- }
- }
- }
- function reportDuplicatesInGroup(
- context: RuleContext<MessageIds, Options>,
- group: { function: FunctionNode; parent?: TSESTree.Node; line: number }[],
- originalFunction: { function: FunctionNode; parent?: TSESTree.Node; line: number },
- ) {
- for (let i = 1; i < group.length; i++) {
- const duplicatingFunction = group[i];
- if (duplicatingFunction && areEquivalent(
- duplicatingFunction.function.body,
- originalFunction.function.body,
- context.sourceCode
- )) {
- const loc = getMainFunctionTokenLocation(
- context.sourceCode,
- duplicatingFunction.function,
- duplicatingFunction.parent,
- );
- context.report({
- messageId: constants.messageIds.mustAvoidCodeDuplicationInFunctions,
- data: {
- line: originalFunction.function.loc!.start.line,
- },
- loc,
- });
- }
- }
- }
- function visitFunction(
- sourceCode: TSESLint.SourceCode,
- node: FunctionNode,
- functions: Functions,
- minLines: number,
- ) {
- let isBigEnough = false;
- const srcTokens = sourceCode.getTokens(node.body);
- if (srcTokens.length > 0 && srcTokens[0]?.value === tokens.openBrace) {
- srcTokens.shift();
- }
- if (srcTokens.length > 0 && srcTokens[srcTokens.length - 1]?.value === tokens.closeBrace) {
- srcTokens.pop();
- }
- if (srcTokens.length > 0) {
- const firstLine = srcTokens[0]?.loc.start.line;
- const lastLine = srcTokens[srcTokens.length - 1]?.loc.end.line;
- if (lastLine && firstLine) {
- isBigEnough = lastLine - firstLine + 1 >= minLines;
- }
- }
- if (isBigEnough) {
- functions.push({ function: node, parent: node.parent });
- }
- }
- const createRule = ESLintUtils.RuleCreator((ruleName) => `${constants.url}${ruleName}`);
- const mustAvoidCodeDuplicationInFunctions = createRule<Options, MessageIds>({
- name: constants.ruleName,
- meta: constants.meta,
- defaultOptions: [{ minLines: defaultMinLines }],
- create(context) {
- const functions: Functions = [];
- const minLines = context.options[0]?.minLines ?? defaultMinLines;
- return {
- FunctionDeclaration(node) {
- visitFunction(context.sourceCode, node, functions, minLines);
- },
- "VariableDeclarator > FunctionExpression, MethodDefinition > FunctionExpression": (
- node: TSESTree.FunctionExpression,
- ) => {
- visitFunction(context.sourceCode, node, functions, minLines);
- },
- "VariableDeclarator > ArrowFunctionExpression, MethodDefinition > ArrowFunctionExpression": (
- node: TSESTree.ArrowFunctionExpression,
- ) => {
- visitFunction(context.sourceCode, node, functions, minLines);
- },
- "Property > FunctionExpression, Property > ArrowFunctionExpression": (
- node: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,
- ) => {
- visitFunction(context.sourceCode, node, functions, minLines);
- },
- "Program:exit"() {
- processFunctions(context, functions);
- },
- };
- },
- });
- export default mustAvoidCodeDuplicationInFunctions;
Advertisement
Add Comment
Please, Sign In to add comment