Skip to content

Commit 7afeae2

Browse files
update require-version-in-deprecation for new deprecation policy (#102)
* update require-version-in-deprecation for new deprecation policy * use corepack in workflows * write tests for default settings, fix bugs * add more test cases * require complete versions * report all problems in one go * Change files
1 parent 6eb0a5a commit 7afeae2

12 files changed

Lines changed: 990 additions & 196 deletions

.github/workflows/ci-build.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ jobs:
2020
node-version: 20
2121
registry-url: https://registry.npmjs.org/
2222

23-
- name: Install pnpm
24-
uses: pnpm/action-setup@v4
25-
with:
26-
version: 9.12.0
23+
- name: Enable corepack
24+
run: corepack enable
2725

2826
- name: Install dependencies
2927
run: pnpm install

.github/workflows/publish-dev.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ jobs:
2222
git config --local user.email imodeljs-admin@users.noreply.github.com
2323
git config --local user.name imodeljs-admin
2424
25-
- name: Install pnpm
26-
uses: pnpm/action-setup@v4
27-
with:
28-
version: 9.12.0
25+
- name: Enable corepack
26+
run: corepack enable
2927

3028
- name: Install dependencies
3129
run: pnpm install --frozen-lockfile=true

.github/workflows/publish.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ jobs:
2222
git config --local user.email imodeljs-admin@users.noreply.github.com
2323
git config --local user.name imodeljs-admin
2424
25-
- name: Install pnpm
26-
uses: pnpm/action-setup@v4
27-
with:
28-
version: 9.12.0
25+
- name: Enable corepack
26+
run: corepack enable
2927

3028
- name: Install dependencies
3129
run: pnpm install --frozen-lockfile=true

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"tabWidth": 2,
3+
"useTabs": false,
4+
"printWidth": 150,
5+
"trailingComma": "all"
6+
}

.vscode/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"[javascript]": {
3+
"editor.defaultFormatter": "esbenp.prettier-vscode"
4+
},
5+
"[jsonc]": {
6+
"editor.defaultFormatter": "esbenp.prettier-vscode"
7+
},
8+
"editor.formatOnSave": true
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "update require-version-in-deprecation for new deprecation policy",
4+
"packageName": "@itwin/eslint-plugin",
5+
"email": "66480813+paulius-valiunas@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 172 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,187 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3-
* See LICENSE.md in the project root for license terms and full copyright notice.
4-
*--------------------------------------------------------------------------------------------*/
2+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3+
* See LICENSE.md in the project root for license terms and full copyright notice.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* @import { Rule, Scope, AST } from "eslint"
8+
*/
59

610
//Custom rule that enforces version and description for @deprecated
711

812
"use strict";
913

14+
const { DateTime } = require("luxon");
15+
16+
const regexParts = {
17+
expired: "might be removed in next major version",
18+
notUntil: "will not be removed until",
19+
};
20+
21+
const deprecatedCommentRegex = new RegExp(
22+
[
23+
/@deprecated(?=[\W])(?: in (?<version>\d+(?:\.(?:\d|x)+){1,2}))?/.source,
24+
`(?: - (?<when>(?:${regexParts.expired})|(?:${regexParts.notUntil} (?<date>[0-9]{4}(?:-[0-9]{2}){2}))))?`,
25+
/(?:(?<separator>\.[^\S\r\n])?(?:[^\S\r\n]*)?(?<description>(?=[\S]).{5,}))?/.source,
26+
].join(""),
27+
"gdu",
28+
);
29+
30+
const validDescriptionRegex = /^[\w\]`]/;
31+
32+
const messages = {
33+
noDescription: "@deprecated must be followed by deprecation reason and/or alternative API usage required",
34+
doubleDeprecation: "Multiple @deprecated notes found in the same comment",
35+
oldDate: `Expired deprecation date was found and should be replaced with "${regexParts.expired}"`,
36+
noDate: `@deprecated should be followed by either "${regexParts.notUntil} {YYYY-MM-dd}" or "${regexParts.expired}"`,
37+
noVersion: `@deprecated should be followed by "in {major}.{minor}`,
38+
noSeparator:
39+
"Deprecation reason should be separated from any preceding text by `. ` ONLY if there is a version or other information before the description",
40+
badDescription: `Deprecation reason should match regex ${validDescriptionRegex.source.replace("\\\\", "\\")}`,
41+
badVersion: "Version numbers should be complete - no 'x' allowed",
42+
};
43+
44+
/** @type {typeof messages} */
45+
const messageIds = Object.keys(messages).reduce((obj, key) => {
46+
obj[key] = key;
47+
return obj;
48+
}, {});
49+
50+
/** @param {string} str */
51+
function firstUpper(str) {
52+
return `${str[0].toUpperCase()}${str.substring(1)}`;
53+
}
54+
55+
/**
56+
* @param {Rule.RuleFixer} fixer
57+
* @param {AST.Program["comments"][0]} comment
58+
* @param {string} newText
59+
*/
60+
function wrapComment(fixer, comment, newText) {
61+
// @ts-expect-error -- comments are not considered nodes but this still works
62+
return fixer.replaceText(comment, comment.type === "Block" ? `/*${newText}*/` : `//${newText}`);
63+
}
64+
65+
/** @type {Rule.RuleModule & {messageIds: typeof messageIds}} */
1066
module.exports = {
1167
meta: {
12-
messages: {
13-
requireVersionAndSentence: "@deprecated in Major.minor format followed by deprecation reason and/or alternative API usage required"
14-
}
68+
type: "problem",
69+
messages,
70+
fixable: "whitespace",
71+
schema: [
72+
{
73+
type: "object",
74+
properties: {
75+
removeOldDates: {
76+
type: "boolean",
77+
},
78+
addVersion: {
79+
type: "string",
80+
},
81+
},
82+
additionalProperties: false,
83+
},
84+
],
1585
},
1686
create(context) {
1787
return {
18-
Program(node) {
19-
let match;
20-
for (const comment of node.comments) {
21-
if (match = /@deprecated(?<in> in \d+\.(\d|x)+[.,\s](?<sentence>.+))?/.exec(comment.value)) {
22-
if ((match?.groups?.in) && (match?.groups?.sentence && match?.groups?.sentence.replace(/\s/g, '').length > 5)) {
23-
continue;
88+
Program(program) {
89+
let regexMatch;
90+
for (const comment of program.comments ?? []) {
91+
let found = 0;
92+
/** @type {Rule.Node} */
93+
// @ts-expect-error -- comments are not considered nodes but this still works
94+
const node = comment;
95+
/** @type {Scope.Scope["block"]} */
96+
while ((regexMatch = deprecatedCommentRegex.exec(comment.value))) {
97+
const match = regexMatch;
98+
if (found === 1) {
99+
context.report({
100+
node,
101+
messageId: messageIds.doubleDeprecation,
102+
});
103+
}
104+
found++;
105+
if (!match.groups?.description) {
106+
context.report({
107+
node,
108+
messageId: messageIds.noDescription,
109+
});
110+
continue; // no point in applying any fixes after this - the description will have to be added manually anyway.
111+
}
112+
113+
let noVersion = false;
114+
let addDate = false;
115+
let oldDate = false;
116+
let addSeparator = false;
117+
let removeSeparator = false;
118+
let badDescription = false;
119+
if (!match.groups?.version) {
120+
// the version is missing
121+
if (context.options[0]?.addVersion || match.groups?.when) {
122+
// either the options require having a version, or the deprecation date is already there, in both cases the version should also be there.
123+
noVersion = true;
124+
}
125+
}
126+
127+
if (match.groups?.version?.includes("x")) {
128+
context.report({
129+
node,
130+
messageId: messageIds.badVersion,
131+
});
24132
}
25-
else {
26-
context.report({ node: comment, messageId: "requireVersionAndSentence" });
133+
134+
const now = DateTime.now();
135+
const currentDate = now.toFormat("yyyy-MM-dd");
136+
const targetDate = now.plus({ year: 1 }).toFormat("yyyy-MM-dd");
137+
138+
if (context.options[0]?.addVersion && !match.groups?.when) {
139+
// add date
140+
addDate = true;
141+
}
142+
143+
if (context.options[0]?.removeOldDates && match.groups?.date && match.groups.date < currentDate) {
144+
// remove old date
145+
oldDate = true;
27146
}
147+
148+
if (match.groups?.description && !validDescriptionRegex.test(match.groups.description)) badDescription = true;
149+
150+
if ((match.groups?.version || match.groups?.when) && !match.groups?.separator && match.groups?.description) addSeparator = true;
151+
else if (!(match.groups?.version || match.groups?.when) && match.groups?.separator && match.groups?.description) removeSeparator = true;
152+
153+
const oldDescription = match.groups?.description?.trimStart() ?? "";
154+
const newDescription = firstUpper(oldDescription.replace(/^-\s*/, ""));
155+
const newComment =
156+
comment.value.substring(0, match.index) +
157+
"@deprecated" +
158+
(noVersion && context.options[0]?.addVersion
159+
? ` in ${context.options[0].addVersion}`
160+
: match.groups?.version
161+
? ` in ${match.groups.version}`
162+
: "") +
163+
(addDate
164+
? ` - ${regexParts.notUntil} ${targetDate}`
165+
: oldDate
166+
? ` - ${regexParts.expired}`
167+
: match.groups?.when
168+
? ` - ${match.groups.when}`
169+
: "") +
170+
(addDate || oldDate || match.groups?.when || noVersion || match.groups?.version ? "." : "") +
171+
` ${newDescription}` +
172+
comment.value.substring(match.index + match[0].length);
173+
174+
/** @param {Rule.RuleFixer} fixer */
175+
const fix = newComment !== comment.value ? (fixer) => wrapComment(fixer, comment, newComment) : undefined;
176+
if (noVersion) context.report({ node, messageId: messageIds.noVersion, fix });
177+
if (addDate) context.report({ node, messageId: messageIds.noDate, fix });
178+
if (oldDate) context.report({ node, messageId: messageIds.oldDate, fix });
179+
if (addSeparator || removeSeparator) context.report({ node, messageId: messageIds.noSeparator, fix });
180+
if (badDescription) context.report({ node, messageId: messageIds.badDescription, fix });
28181
}
29182
}
30-
}
31-
}
32-
}
33-
}
183+
},
184+
};
185+
},
186+
messageIds,
187+
};

jsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"compilerOptions": {
33
"checkJs": true,
44
"noEmit": true,
5-
"target": "es6", // need for using for...of on iterables
6-
"lib": ["es6"],
5+
"target": "es2022", // need for using for...of on iterables and named groups in regex
6+
"lib": ["es2022"],
77
"strict": true,
88
"useUnknownInCatchVariables": false,
99
"noImplicitAny": false,

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,23 @@
4545
"eslint-plugin-prefer-arrow": "^1.2.3",
4646
"eslint-plugin-react": "^7.37.4",
4747
"eslint-plugin-react-hooks": "^5.2.0",
48+
"luxon": "^3.6.1",
4849
"workspace-tools": "^0.36.4"
4950
},
5051
"peerDependencies": {
5152
"eslint": "^9.11.1",
5253
"typescript": "^3.7.0 || ^4.0.0 || ^5.0.0"
5354
},
5455
"devDependencies": {
55-
"@types/eslint": "~9.6.1",
5656
"@types/node": "20.11.16",
5757
"beachball": "^2.51.0",
58-
"eslint": "^9.22.0",
58+
"eslint": "^9.26.0",
5959
"husky": "^9.1.7",
6060
"mocha": "^10.8.2",
6161
"typescript": "~5.6.3"
6262
},
6363
"engines": {
6464
"node": "^18.18.0 || ^20.0.0 || ^22.0.0"
65-
}
65+
},
66+
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
6667
}

0 commit comments

Comments
 (0)