Skip to content

Commit ac51d40

Browse files
authoredOct 29, 2023
expiring-todo-comments: Support monorepos (#2159)
1 parent 0a4d70a commit ac51d40

File tree

1 file changed

+122
-104
lines changed

1 file changed

+122
-104
lines changed
 

‎rules/expiring-todo-comments.js

+122-104
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const path = require('node:path');
23
const readPkgUp = require('read-pkg-up');
34
const semver = require('semver');
45
const ci = require('ci-info');
@@ -47,146 +48,160 @@ const messages = {
4748
'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.',
4849
};
4950

50-
// We don't need to normalize the package.json data, because we are only using 2 properties and those 2 properties
51-
// aren't validated by the normalization. But when this plugin is used in a monorepo, the name field in the
52-
// package.json is invalid and would make this plugin throw an error. See also #1871
53-
const packageResult = readPkgUp.sync({normalize: false});
54-
const hasPackage = Boolean(packageResult);
55-
const packageJson = hasPackage ? packageResult.packageJson : {};
56-
57-
const packageDependencies = {
58-
...packageJson.dependencies,
59-
...packageJson.devDependencies,
60-
};
61-
62-
const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/;
63-
const VERSION_COMPARISON_RE = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
64-
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/;
65-
const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/;
66-
67-
function parseTodoWithArguments(string, {terms}) {
68-
const lowerCaseString = string.toLowerCase();
69-
const lowerCaseTerms = terms.map(term => term.toLowerCase());
70-
const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term));
71-
72-
if (!hasTerm) {
73-
return false;
51+
/** @param {string} dirname */
52+
function getPackageHelpers(dirname) {
53+
// We don't need to normalize the package.json data, because we are only using 2 properties and those 2 properties
54+
// aren't validated by the normalization. But when this plugin is used in a monorepo, the name field in the
55+
// package.json can be invalid and would make this plugin throw an error. See also #1871
56+
/** @type {readPkgUp.ReadResult | undefined} */
57+
let packageResult;
58+
try {
59+
packageResult = readPkgUp.sync({normalize: false, cwd: dirname});
60+
} catch {
61+
// This can happen if package.json files have comments in them etc.
62+
packageResult = undefined;
7463
}
7564

76-
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
77-
const result = TODO_ARGUMENT_RE.exec(string);
78-
79-
if (!result) {
80-
return false;
81-
}
65+
const hasPackage = Boolean(packageResult);
66+
const packageJson = packageResult ? packageResult.packageJson : {};
8267

83-
const {rawArguments} = result.groups;
68+
const packageDependencies = {
69+
...packageJson.dependencies,
70+
...packageJson.devDependencies,
71+
};
8472

85-
const parsedArguments = rawArguments
86-
.split(',')
87-
.map(argument => parseArgument(argument.trim()));
73+
function parseTodoWithArguments(string, {terms}) {
74+
const lowerCaseString = string.toLowerCase();
75+
const lowerCaseTerms = terms.map(term => term.toLowerCase());
76+
const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term));
8877

89-
return createArgumentGroup(parsedArguments);
90-
}
78+
if (!hasTerm) {
79+
return false;
80+
}
9181

92-
function createArgumentGroup(arguments_) {
93-
const groups = {};
94-
for (const {value, type} of arguments_) {
95-
groups[type] = groups[type] || [];
96-
groups[type].push(value);
97-
}
82+
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
83+
const result = TODO_ARGUMENT_RE.exec(string);
9884

99-
return groups;
100-
}
85+
if (!result) {
86+
return false;
87+
}
10188

102-
function parseArgument(argumentString) {
103-
if (ISO8601_DATE.test(argumentString)) {
104-
return {
105-
type: 'dates',
106-
value: argumentString,
107-
};
108-
}
89+
const {rawArguments} = result.groups;
10990

110-
if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
111-
const condition = argumentString[0] === '+' ? 'in' : 'out';
112-
const name = argumentString.slice(1).trim();
91+
const parsedArguments = rawArguments
92+
.split(',')
93+
.map(argument => parseArgument(argument.trim()));
11394

114-
return {
115-
type: 'dependencies',
116-
value: {
117-
name,
118-
condition,
119-
},
120-
};
95+
return createArgumentGroup(parsedArguments);
12196
}
12297

123-
if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) {
124-
const {groups} = VERSION_COMPARISON_RE.exec(argumentString);
125-
const name = groups.name.trim();
126-
const condition = groups.condition.trim();
127-
const version = groups.version.trim();
98+
function parseArgument(argumentString, dirname) {
99+
const {hasPackage} = getPackageHelpers(dirname);
100+
if (ISO8601_DATE.test(argumentString)) {
101+
return {
102+
type: 'dates',
103+
value: argumentString,
104+
};
105+
}
128106

129-
const hasEngineKeyword = name.indexOf('engine:') === 0;
130-
const isNodeEngine = hasEngineKeyword && name === 'engine:node';
107+
if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
108+
const condition = argumentString[0] === '+' ? 'in' : 'out';
109+
const name = argumentString.slice(1).trim();
131110

132-
if (hasEngineKeyword && isNodeEngine) {
133111
return {
134-
type: 'engines',
112+
type: 'dependencies',
135113
value: {
114+
name,
136115
condition,
137-
version,
138116
},
139117
};
140118
}
141119

142-
if (!hasEngineKeyword) {
120+
if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) {
121+
const {groups} = VERSION_COMPARISON_RE.exec(argumentString);
122+
const name = groups.name.trim();
123+
const condition = groups.condition.trim();
124+
const version = groups.version.trim();
125+
126+
const hasEngineKeyword = name.indexOf('engine:') === 0;
127+
const isNodeEngine = hasEngineKeyword && name === 'engine:node';
128+
129+
if (hasEngineKeyword && isNodeEngine) {
130+
return {
131+
type: 'engines',
132+
value: {
133+
condition,
134+
version,
135+
},
136+
};
137+
}
138+
139+
if (!hasEngineKeyword) {
140+
return {
141+
type: 'dependencies',
142+
value: {
143+
name,
144+
condition,
145+
version,
146+
},
147+
};
148+
}
149+
}
150+
151+
if (hasPackage && PKG_VERSION_RE.test(argumentString)) {
152+
const result = PKG_VERSION_RE.exec(argumentString);
153+
const {condition, version} = result.groups;
154+
143155
return {
144-
type: 'dependencies',
156+
type: 'packageVersions',
145157
value: {
146-
name,
147-
condition,
148-
version,
158+
condition: condition.trim(),
159+
version: version.trim(),
149160
},
150161
};
151162
}
152-
}
153-
154-
if (hasPackage && PKG_VERSION_RE.test(argumentString)) {
155-
const result = PKG_VERSION_RE.exec(argumentString);
156-
const {condition, version} = result.groups;
157163

164+
// Currently being ignored as integration tests pointed
165+
// some TODO comments have `[random data like this]`
158166
return {
159-
type: 'packageVersions',
160-
value: {
161-
condition: condition.trim(),
162-
version: version.trim(),
163-
},
167+
type: 'unknowns',
168+
value: argumentString,
164169
};
165170
}
166171

167-
// Currently being ignored as integration tests pointed
168-
// some TODO comments have `[random data like this]`
169-
return {
170-
type: 'unknowns',
171-
value: argumentString,
172-
};
173-
}
172+
function parseTodoMessage(todoString) {
173+
// @example "TODO [...]: message here"
174+
// @example "TODO [...] message here"
175+
const argumentsEnd = todoString.indexOf(']');
176+
177+
const afterArguments = todoString.slice(argumentsEnd + 1).trim();
178+
179+
// Check if have to skip colon
180+
// @example "TODO [...]: message here"
181+
const dropColon = afterArguments[0] === ':';
182+
if (dropColon) {
183+
return afterArguments.slice(1).trim();
184+
}
174185

175-
function parseTodoMessage(todoString) {
176-
// @example "TODO [...]: message here"
177-
// @example "TODO [...] message here"
178-
const argumentsEnd = todoString.indexOf(']');
186+
return afterArguments;
187+
}
179188

180-
const afterArguments = todoString.slice(argumentsEnd + 1).trim();
189+
return {packageResult, hasPackage, packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments};
190+
}
191+
192+
const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/;
193+
const VERSION_COMPARISON_RE = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
194+
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/;
195+
const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/;
181196

182-
// Check if have to skip colon
183-
// @example "TODO [...]: message here"
184-
const dropColon = afterArguments[0] === ':';
185-
if (dropColon) {
186-
return afterArguments.slice(1).trim();
197+
function createArgumentGroup(arguments_) {
198+
const groups = {};
199+
for (const {value, type} of arguments_) {
200+
groups[type] = groups[type] || [];
201+
groups[type].push(value);
187202
}
188203

189-
return afterArguments;
204+
return groups;
190205
}
191206

192207
function reachedDate(past, now) {
@@ -263,6 +278,9 @@ const create = context => {
263278
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'),
264279
);
265280

281+
const dirname = path.dirname(context.filename);
282+
const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname);
283+
266284
const {sourceCode} = context;
267285
const comments = sourceCode.getAllComments();
268286
const unusedComments = comments

0 commit comments

Comments
 (0)
Please sign in to comment.