Skip to content

Commit

Permalink
expiring-todo-comments: Support monorepos (#2159)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Oct 29, 2023
1 parent 0a4d70a commit ac51d40
Showing 1 changed file with 122 additions and 104 deletions.
226 changes: 122 additions & 104 deletions rules/expiring-todo-comments.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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 = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\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 = /\[(?<rawArguments>[^}]+)]/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 = /\[(?<rawArguments>[^}]+)]/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 = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\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) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ac51d40

Please sign in to comment.