diff --git a/README.md b/README.md index 233dcd0..c3e239e 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,27 @@ class ClassName { } ``` +## Configuration + +By default the rule applies to all functions. You can optionally provide an `ignoreList` list of regex patterns — any function whose name matches a pattern is exempt from the rule. + +```js +rules: { + 'observation/no-function-without-logging': [ + 'error', + { + ignoreList: [ + '^handle[A-Z].*', // e.g. handleClick, handleChange + '^on[A-Z].*', // e.g. onChange, onPress + 'mySpecificFunction', + ], + } + ] +} +``` + +Each entry in `ignoreList` is interpreted as a regular expression and tested against the function name. Functions skipped by other built-in exceptions (constructors, getters/setters, setter-like methods, component declarations) are unaffected by this option. + ## No Missing Translations This ESLint rule ensures that every call to `i18n.t(...)` in the codebase has a corresponding key in all translation files. A translation file is defined as an input file for the npm package `i18n-js` (https://www.npmjs.com/package/i18n-js). diff --git a/dist/rules/__tests__/no-function-without-logging.test.js b/dist/rules/__tests__/no-function-without-logging.test.js index 90d2af3..835d4f5 100644 --- a/dist/rules/__tests__/no-function-without-logging.test.js +++ b/dist/rules/__tests__/no-function-without-logging.test.js @@ -66,6 +66,36 @@ ruleTester.run("no-function-without-logging", no_function_without_logging_1.defa filename: "Component", code: "const Component = () => { Log.debug('Component') }", }, + { + name: "Ignored function declaration is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "function ignoredFunction(){}", + }, + { + name: "Ignored arrow function in variable declaration is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "const ignoredFunction = () => { }", + }, + { + name: "Ignored class method is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "class ClassName { ignoredFunction(){ } }", + }, + { + name: "Ignored class property arrow function is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "class ClassName { ignoredFunction = () => { } }", + }, + { + name: "Ignored function matched by regex pattern is skipped", + options: [{ ignoreList: ["^handle[A-Z].*"] }], + code: "function handleClick(){}", + }, + { + name: "Multiple ignore patterns, one matches", + options: [{ ignoreList: ["^handle[A-Z].*", "^on[A-Z].*"] }], + code: "function onChange(){}", + }, ], invalid: [ { diff --git a/dist/rules/no-function-without-logging.js b/dist/rules/no-function-without-logging.js index 2e719ef..8bfd402 100644 --- a/dist/rules/no-function-without-logging.js +++ b/dist/rules/no-function-without-logging.js @@ -38,6 +38,11 @@ const path = __importStar(require("path")); const utils_1 = require("@typescript-eslint/utils"); const utils_2 = require("../utils"); const createRule = utils_1.ESLintUtils.RuleCreator(() => "https://github.com/observation/eslint-rules"); +const isIgnored = (functionName, patterns) => { + if (!functionName || patterns.length === 0) + return false; + return patterns.some((pattern) => new RegExp(pattern).test(functionName)); +}; const createSuggestions = (blockStatement, suggestedLogging) => { const logLevels = ["trace", "debug"]; return logLevels.map((logLevel) => { @@ -111,18 +116,22 @@ const containsLoggingStatement = (blockStatement) => { } return false; }; -const checkFunctionDeclaration = (context, node) => { +const checkFunctionDeclaration = (context, node, ignoreList) => { const functionName = node.id ? node.id.name : ""; + if (isIgnored(functionName, ignoreList)) + return; const file = path.parse(context.getFilename()); const correctLogging = `${file.name}:${functionName}`; if (!containsLoggingStatement(node.body)) { addMissingLogStatementSuggestions(context, node, node.body, correctLogging); } }; -const checkCallExpression = (context, node) => { +const checkCallExpression = (context, node, ignoreList) => { if (isLogStatement(node)) { const filename = path.parse(context.getFilename()).name; const functionName = getFunctionName(node); + if (isIgnored(functionName, ignoreList)) + return; const expectedLogging = filename === functionName ? filename : `${filename}:${functionName}`; const [argument] = node.arguments; if (!argument) { @@ -163,7 +172,7 @@ const checkCallExpression = (context, node) => { } } }; -const checkVariableDeclaration = (context, node) => { +const checkVariableDeclaration = (context, node, ignoreList) => { if (node.declarations.length !== 1) return; const [declaration] = node.declarations; @@ -177,21 +186,25 @@ const checkVariableDeclaration = (context, node) => { const isComponentDeclaration = filename === functionName; if (isComponentDeclaration) return; + if (isIgnored(functionName, ignoreList)) + return; if (!containsLoggingStatement(body)) { const correctLogging = `${filename}:${functionName}`; addMissingLogStatementSuggestions(context, node, body, correctLogging); } } }; -const checkPropertyDefinition = (context, node) => { +const checkPropertyDefinition = (context, node, ignoreList) => { if (node.value && (0, utils_2.isArrowFunctionExpression)(node.value) && (0, utils_2.isIdentifier)(node.key) && (0, utils_2.isBlockStatement)(node.value.body)) { const { body } = node.value; + const filename = path.parse(context.getFilename()).name; + const functionName = node.key.name; + if (isIgnored(functionName, ignoreList)) + return; if (!containsLoggingStatement(body)) { - const filename = path.parse(context.getFilename()).name; - const functionName = node.key.name; const correctLogging = filename === functionName ? filename : `${filename}:${functionName}`; addMissingLogStatementSuggestions(context, node, body, correctLogging); } @@ -205,7 +218,7 @@ const isSetterLikeMethodDefinition = (node, functionName) => { const hasSetterLikeFunctionName = startsWithSetterLikeName.test(functionName); return hasSetterLikeFunctionName && returnsVoid; }; -const checkMethodDefinition = (context, node) => { +const checkMethodDefinition = (context, node, ignoreList) => { if (node.kind === "constructor") return; if (node.kind === "get") @@ -214,11 +227,13 @@ const checkMethodDefinition = (context, node) => { return; if ((0, utils_2.isFunctionExpression)(node.value) && (0, utils_2.isIdentifier)(node.key)) { const { body } = node.value; + const filename = path.parse(context.getFilename()).name; + const functionName = node.key.name; + if (isSetterLikeMethodDefinition(node, functionName)) + return; + if (isIgnored(functionName, ignoreList)) + return; if (!containsLoggingStatement(body)) { - const filename = path.parse(context.getFilename()).name; - const functionName = node.key.name; - if (isSetterLikeMethodDefinition(node, functionName)) - return; const correctLogging = filename === functionName ? filename : `${filename}:${functionName}`; addMissingLogStatementSuggestions(context, node, body, correctLogging); } @@ -226,12 +241,13 @@ const checkMethodDefinition = (context, node) => { }; const noFunctionWithoutLogging = createRule({ create(context) { + const ignoreList = context.options[0]?.ignoreList ?? []; return { - FunctionDeclaration: (node) => checkFunctionDeclaration(context, node), - CallExpression: (node) => checkCallExpression(context, node), - VariableDeclaration: (node) => checkVariableDeclaration(context, node), - PropertyDefinition: (node) => checkPropertyDefinition(context, node), - MethodDefinition: (node) => checkMethodDefinition(context, node), + FunctionDeclaration: (node) => checkFunctionDeclaration(context, node, ignoreList), + CallExpression: (node) => checkCallExpression(context, node, ignoreList), + VariableDeclaration: (node) => checkVariableDeclaration(context, node, ignoreList), + PropertyDefinition: (node) => checkPropertyDefinition(context, node, ignoreList), + MethodDefinition: (node) => checkMethodDefinition(context, node, ignoreList), }; }, name: "no-function-without-logging", @@ -246,10 +262,22 @@ const noFunctionWithoutLogging = createRule({ }, type: "suggestion", fixable: "code", - schema: [], + schema: [ + { + type: "object", + properties: { + ignoreList: { + type: "array", + items: { type: "string" }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], hasSuggestions: true, }, - defaultOptions: [], + defaultOptions: [{}], }); exports.configs = { recommended: { diff --git a/src/rules/__tests__/no-function-without-logging.test.ts b/src/rules/__tests__/no-function-without-logging.test.ts index 9531d6d..9df34c6 100644 --- a/src/rules/__tests__/no-function-without-logging.test.ts +++ b/src/rules/__tests__/no-function-without-logging.test.ts @@ -63,6 +63,36 @@ ruleTester.run("no-function-without-logging", rule, { filename: "Component", code: "const Component = () => { Log.debug('Component') }", }, + { + name: "Ignored function declaration is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "function ignoredFunction(){}", + }, + { + name: "Ignored arrow function in variable declaration is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "const ignoredFunction = () => { }", + }, + { + name: "Ignored class method is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "class ClassName { ignoredFunction(){ } }", + }, + { + name: "Ignored class property arrow function is skipped", + options: [{ ignoreList: ["ignoredFunction"] }], + code: "class ClassName { ignoredFunction = () => { } }", + }, + { + name: "Ignored function matched by regex pattern is skipped", + options: [{ ignoreList: ["^handle[A-Z].*"] }], + code: "function handleClick(){}", + }, + { + name: "Multiple ignore patterns, one matches", + options: [{ ignoreList: ["^handle[A-Z].*", "^on[A-Z].*"] }], + code: "function onChange(){}", + }, ], invalid: [ { diff --git a/src/rules/no-function-without-logging.ts b/src/rules/no-function-without-logging.ts index 409e093..b266f16 100644 --- a/src/rules/no-function-without-logging.ts +++ b/src/rules/no-function-without-logging.ts @@ -22,6 +22,13 @@ const createRule = ESLintUtils.RuleCreator( type messageIds = "incorrectLogging" | "missingLogging" | "addLoggingSuggestion" +type Options = [{ ignoreList?: string[] }] + +const isIgnored = (functionName: string | null, patterns: string[]): boolean => { + if (!functionName || patterns.length === 0) return false + return patterns.some((pattern) => new RegExp(pattern).test(functionName)) +} + const createSuggestions = ( blockStatement: TSESTree.BlockStatement, suggestedLogging: string @@ -123,10 +130,13 @@ const containsLoggingStatement = ( } const checkFunctionDeclaration = ( - context: Readonly>, - node: TSESTree.FunctionDeclaration + context: Readonly>, + node: TSESTree.FunctionDeclaration, + ignoreList: string[] ) => { const functionName = node.id ? node.id.name : "" + if (isIgnored(functionName, ignoreList)) return + const file = path.parse(context.getFilename()) const correctLogging = `${file.name}:${functionName}` @@ -136,12 +146,14 @@ const checkFunctionDeclaration = ( } const checkCallExpression = ( - context: Readonly>, - node: TSESTree.CallExpression + context: Readonly>, + node: TSESTree.CallExpression, + ignoreList: string[] ) => { if (isLogStatement(node)) { const filename = path.parse(context.getFilename()).name const functionName = getFunctionName(node) + if (isIgnored(functionName, ignoreList)) return const expectedLogging = filename === functionName ? filename : `${filename}:${functionName}` const [argument] = node.arguments @@ -192,8 +204,9 @@ const checkCallExpression = ( } const checkVariableDeclaration = ( - context: Readonly>, - node: TSESTree.VariableDeclaration + context: Readonly>, + node: TSESTree.VariableDeclaration, + ignoreList: string[] ) => { if (node.declarations.length !== 1) return @@ -211,6 +224,7 @@ const checkVariableDeclaration = ( const isComponentDeclaration = filename === functionName if (isComponentDeclaration) return + if (isIgnored(functionName, ignoreList)) return if (!containsLoggingStatement(body)) { const correctLogging = `${filename}:${functionName}` @@ -220,8 +234,9 @@ const checkVariableDeclaration = ( } const checkPropertyDefinition = ( - context: Readonly>, - node: TSESTree.PropertyDefinition + context: Readonly>, + node: TSESTree.PropertyDefinition, + ignoreList: string[] ) => { if ( node.value && @@ -230,10 +245,12 @@ const checkPropertyDefinition = ( isBlockStatement(node.value.body) ) { const { body } = node.value + const filename = path.parse(context.getFilename()).name + const functionName = node.key.name + + if (isIgnored(functionName, ignoreList)) return if (!containsLoggingStatement(body)) { - const filename = path.parse(context.getFilename()).name - const functionName = node.key.name const correctLogging = filename === functionName ? filename : `${filename}:${functionName}` addMissingLogStatementSuggestions(context, node, body, correctLogging) @@ -257,8 +274,9 @@ const isSetterLikeMethodDefinition = ( } const checkMethodDefinition = ( - context: Readonly>, - node: TSESTree.MethodDefinition + context: Readonly>, + node: TSESTree.MethodDefinition, + ignoreList: string[] ) => { if (node.kind === "constructor") return if (node.kind === "get") return @@ -266,12 +284,13 @@ const checkMethodDefinition = ( if (isFunctionExpression(node.value) && isIdentifier(node.key)) { const { body } = node.value - if (!containsLoggingStatement(body)) { - const filename = path.parse(context.getFilename()).name - const functionName = node.key.name + const filename = path.parse(context.getFilename()).name + const functionName = node.key.name - if (isSetterLikeMethodDefinition(node, functionName)) return + if (isSetterLikeMethodDefinition(node, functionName)) return + if (isIgnored(functionName, ignoreList)) return + if (!containsLoggingStatement(body)) { const correctLogging = filename === functionName ? filename : `${filename}:${functionName}` addMissingLogStatementSuggestions(context, node, body, correctLogging) @@ -279,14 +298,15 @@ const checkMethodDefinition = ( } } -const noFunctionWithoutLogging = createRule({ +const noFunctionWithoutLogging = createRule({ create(context) { + const ignoreList = context.options[0]?.ignoreList ?? [] return { - FunctionDeclaration: (node) => checkFunctionDeclaration(context, node), - CallExpression: (node) => checkCallExpression(context, node), - VariableDeclaration: (node) => checkVariableDeclaration(context, node), - PropertyDefinition: (node) => checkPropertyDefinition(context, node), - MethodDefinition: (node) => checkMethodDefinition(context, node), + FunctionDeclaration: (node) => checkFunctionDeclaration(context, node, ignoreList), + CallExpression: (node) => checkCallExpression(context, node, ignoreList), + VariableDeclaration: (node) => checkVariableDeclaration(context, node, ignoreList), + PropertyDefinition: (node) => checkPropertyDefinition(context, node, ignoreList), + MethodDefinition: (node) => checkMethodDefinition(context, node, ignoreList), } }, name: "no-function-without-logging", @@ -303,10 +323,22 @@ const noFunctionWithoutLogging = createRule({ }, type: "suggestion", fixable: "code", - schema: [], + schema: [ + { + type: "object", + properties: { + ignoreList: { + type: "array", + items: { type: "string" }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], hasSuggestions: true, }, - defaultOptions: [], + defaultOptions: [{}], }) export const configs = {