diff --git a/rules/expiring-todo-comments.js b/rules/expiring-todo-comments.js index 020a48e622..1a069dc880 100644 --- a/rules/expiring-todo-comments.js +++ b/rules/expiring-todo-comments.js @@ -1,4 +1,5 @@ 'use strict'; +const path = require('node:path'); const readPkgUp = require('read-pkg-up'); const semver = require('semver'); const ci = require('ci-info'); @@ -47,146 +48,160 @@ const messages = { 'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.', }; -// We don't need to normalize the package.json data, because we are only using 2 properties and those 2 properties -// aren't validated by the normalization. But when this plugin is used in a monorepo, the name field in the -// package.json is invalid and would make this plugin throw an error. See also #1871 -const packageResult = readPkgUp.sync({normalize: false}); -const hasPackage = Boolean(packageResult); -const packageJson = hasPackage ? packageResult.packageJson : {}; - -const packageDependencies = { - ...packageJson.dependencies, - ...packageJson.devDependencies, -}; - -const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/; -const VERSION_COMPARISON_RE = /^(?@?\S\/?\S+)@(?>|>=)(?\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i; -const PKG_VERSION_RE = /^(?>|>=)(?\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/; -const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/; - -function parseTodoWithArguments(string, {terms}) { - const lowerCaseString = string.toLowerCase(); - const lowerCaseTerms = terms.map(term => term.toLowerCase()); - const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term)); - - if (!hasTerm) { - return false; +/** @param {string} dirname */ +function getPackageHelpers(dirname) { + // We don't need to normalize the package.json data, because we are only using 2 properties and those 2 properties + // aren't validated by the normalization. But when this plugin is used in a monorepo, the name field in the + // package.json can be invalid and would make this plugin throw an error. See also #1871 + /** @type {readPkgUp.ReadResult | undefined} */ + let packageResult; + try { + packageResult = readPkgUp.sync({normalize: false, cwd: dirname}); + } catch { + // This can happen if package.json files have comments in them etc. + packageResult = undefined; } - const TODO_ARGUMENT_RE = /\[(?[^}]+)]/i; - const result = TODO_ARGUMENT_RE.exec(string); - - if (!result) { - return false; - } + const hasPackage = Boolean(packageResult); + const packageJson = packageResult ? packageResult.packageJson : {}; - const {rawArguments} = result.groups; + const packageDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }; - const parsedArguments = rawArguments - .split(',') - .map(argument => parseArgument(argument.trim())); + function parseTodoWithArguments(string, {terms}) { + const lowerCaseString = string.toLowerCase(); + const lowerCaseTerms = terms.map(term => term.toLowerCase()); + const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term)); - return createArgumentGroup(parsedArguments); -} + if (!hasTerm) { + return false; + } -function createArgumentGroup(arguments_) { - const groups = {}; - for (const {value, type} of arguments_) { - groups[type] = groups[type] || []; - groups[type].push(value); - } + const TODO_ARGUMENT_RE = /\[(?[^}]+)]/i; + const result = TODO_ARGUMENT_RE.exec(string); - return groups; -} + if (!result) { + return false; + } -function parseArgument(argumentString) { - if (ISO8601_DATE.test(argumentString)) { - return { - type: 'dates', - value: argumentString, - }; - } + const {rawArguments} = result.groups; - if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) { - const condition = argumentString[0] === '+' ? 'in' : 'out'; - const name = argumentString.slice(1).trim(); + const parsedArguments = rawArguments + .split(',') + .map(argument => parseArgument(argument.trim())); - return { - type: 'dependencies', - value: { - name, - condition, - }, - }; + return createArgumentGroup(parsedArguments); } - if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) { - const {groups} = VERSION_COMPARISON_RE.exec(argumentString); - const name = groups.name.trim(); - const condition = groups.condition.trim(); - const version = groups.version.trim(); + function parseArgument(argumentString, dirname) { + const {hasPackage} = getPackageHelpers(dirname); + if (ISO8601_DATE.test(argumentString)) { + return { + type: 'dates', + value: argumentString, + }; + } - const hasEngineKeyword = name.indexOf('engine:') === 0; - const isNodeEngine = hasEngineKeyword && name === 'engine:node'; + if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) { + const condition = argumentString[0] === '+' ? 'in' : 'out'; + const name = argumentString.slice(1).trim(); - if (hasEngineKeyword && isNodeEngine) { return { - type: 'engines', + type: 'dependencies', value: { + name, condition, - version, }, }; } - if (!hasEngineKeyword) { + if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) { + const {groups} = VERSION_COMPARISON_RE.exec(argumentString); + const name = groups.name.trim(); + const condition = groups.condition.trim(); + const version = groups.version.trim(); + + const hasEngineKeyword = name.indexOf('engine:') === 0; + const isNodeEngine = hasEngineKeyword && name === 'engine:node'; + + if (hasEngineKeyword && isNodeEngine) { + return { + type: 'engines', + value: { + condition, + version, + }, + }; + } + + if (!hasEngineKeyword) { + return { + type: 'dependencies', + value: { + name, + condition, + version, + }, + }; + } + } + + if (hasPackage && PKG_VERSION_RE.test(argumentString)) { + const result = PKG_VERSION_RE.exec(argumentString); + const {condition, version} = result.groups; + return { - type: 'dependencies', + type: 'packageVersions', value: { - name, - condition, - version, + condition: condition.trim(), + version: version.trim(), }, }; } - } - - if (hasPackage && PKG_VERSION_RE.test(argumentString)) { - const result = PKG_VERSION_RE.exec(argumentString); - const {condition, version} = result.groups; + // Currently being ignored as integration tests pointed + // some TODO comments have `[random data like this]` return { - type: 'packageVersions', - value: { - condition: condition.trim(), - version: version.trim(), - }, + type: 'unknowns', + value: argumentString, }; } - // Currently being ignored as integration tests pointed - // some TODO comments have `[random data like this]` - return { - type: 'unknowns', - value: argumentString, - }; -} + function parseTodoMessage(todoString) { + // @example "TODO [...]: message here" + // @example "TODO [...] message here" + const argumentsEnd = todoString.indexOf(']'); + + const afterArguments = todoString.slice(argumentsEnd + 1).trim(); + + // Check if have to skip colon + // @example "TODO [...]: message here" + const dropColon = afterArguments[0] === ':'; + if (dropColon) { + return afterArguments.slice(1).trim(); + } -function parseTodoMessage(todoString) { - // @example "TODO [...]: message here" - // @example "TODO [...] message here" - const argumentsEnd = todoString.indexOf(']'); + return afterArguments; + } - const afterArguments = todoString.slice(argumentsEnd + 1).trim(); + return {packageResult, hasPackage, packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments}; +} + +const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/; +const VERSION_COMPARISON_RE = /^(?@?\S\/?\S+)@(?>|>=)(?\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i; +const PKG_VERSION_RE = /^(?>|>=)(?\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/; +const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/; - // Check if have to skip colon - // @example "TODO [...]: message here" - const dropColon = afterArguments[0] === ':'; - if (dropColon) { - return afterArguments.slice(1).trim(); +function createArgumentGroup(arguments_) { + const groups = {}; + for (const {value, type} of arguments_) { + groups[type] = groups[type] || []; + groups[type].push(value); } - return afterArguments; + return groups; } function reachedDate(past, now) { @@ -263,6 +278,9 @@ const create = context => { pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'), ); + const dirname = path.dirname(context.filename); + const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname); + const {sourceCode} = context; const comments = sourceCode.getAllComments(); const unusedComments = comments