Skip to content

Commit

Permalink
fix(expiring-todo-comments): support monorepos
Browse files Browse the repository at this point in the history
The basic change: pass the file being linted dirname rather than cwd. Using cwd meant it would find a monorepo's root package directory.
  • Loading branch information
mmkal committed Jun 24, 2023
1 parent 03d24e7 commit ed2c502
Showing 1 changed file with 127 additions and 116 deletions.
243 changes: 127 additions & 116 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('path')
const readPkgUp = require('read-pkg-up');
const semver = require('semver');
const ci = require('ci-info');
Expand Down Expand Up @@ -47,146 +48,153 @@ 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;
}

const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
const result = TODO_ARGUMENT_RE.exec(string);

if (!result) {
return false;
}

const {rawArguments} = result.groups;

const parsedArguments = rawArguments
.split(',')
.map(argument => parseArgument(argument.trim()));

return createArgumentGroup(parsedArguments);
}

function createArgumentGroup(arguments_) {
const groups = {};
for (const {value, type} of arguments_) {
groups[type] = groups[type] || [];
groups[type].push(value);
}

return groups;
}

function parseArgument(argumentString) {
if (ISO8601_DATE.test(argumentString)) {
return {
type: 'dates',
value: argumentString,
};
}
/** @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 is invalid and would make this plugin throw an error. See also #1871
const packageResult = readPkgUp.sync({normalize: false, cwd: dirname});
const hasPackage = Boolean(packageResult);
const packageJson = packageResult ? packageResult.packageJson : {};

const packageDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};

if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
const condition = argumentString[0] === '+' ? 'in' : 'out';
const name = argumentString.slice(1).trim();

return {
type: 'dependencies',
value: {
name,
condition,
},
};
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;
}

const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
const result = TODO_ARGUMENT_RE.exec(string);

if (!result) {
return false;
}

const {rawArguments} = result.groups;

const parsedArguments = rawArguments
.split(',')
.map(argument => parseArgument(argument.trim()));

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();

const hasEngineKeyword = name.indexOf('engine:') === 0;
const isNodeEngine = hasEngineKeyword && name === 'engine:node';

if (hasEngineKeyword && isNodeEngine) {
function parseArgument(argumentString, dirname) {
const {hasPackage} = getPackageHelpers(dirname)
if (ISO8601_DATE.test(argumentString)) {
return {
type: 'engines',
value: {
condition,
version,
},
type: 'dates',
value: argumentString,
};
}

if (!hasEngineKeyword) {

if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
const condition = argumentString[0] === '+' ? 'in' : 'out';
const name = argumentString.slice(1).trim();

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;


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: 'packageVersions',
value: {
condition: condition.trim(),
version: version.trim(),
},
};
}

// 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();
}

return afterArguments;
}

return {packageResult, hasPackage, packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments}
}

function parseTodoMessage(todoString) {
// @example "TODO [...]: message here"
// @example "TODO [...] message here"
const argumentsEnd = todoString.indexOf(']');

const afterArguments = todoString.slice(argumentsEnd + 1).trim();
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 +271,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 ed2c502

Please sign in to comment.