@tsslint/config 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/plugins/ignore.js +189 -32
- package/package.json +5 -6
package/lib/plugins/ignore.js
CHANGED
|
@@ -1,38 +1,181 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.create = create;
|
|
4
|
-
|
|
5
|
-
// `forEachComment`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Inlined comment-trivia walker. Equivalent to ts-api-utils'
|
|
5
|
+
// `forEachComment` but specialized for our needs (no `kind` / `value`
|
|
6
|
+
// fields, no generator overhead, no per-token closure for the
|
|
7
|
+
// trivia-collector). Algorithm matches upstream:
|
|
8
|
+
//
|
|
9
|
+
// 1. Iterative DFS over `node.getChildren(sourceFile)` — token-only
|
|
10
|
+
// leaves drive the walk. Realizes the AST tree, but the lint
|
|
11
|
+
// pipeline does this anyway for type-aware rules so the cost is
|
|
12
|
+
// amortized across the whole pass.
|
|
13
|
+
// 2. For each token, scan `forEachLeadingCommentRange` (skip shebang
|
|
14
|
+
// at file start) and `forEachTrailingCommentRange`.
|
|
15
|
+
// 3. JSX needs special care: `JsxText` tokens can't carry leading
|
|
16
|
+
// trivia, and trailing trivia rules differ for `}` / `>` tokens
|
|
17
|
+
// based on their JSX-element context.
|
|
18
|
+
//
|
|
19
|
+
// Trivia regions sit BETWEEN tokens — they can't contain string,
|
|
20
|
+
// template, or regex literals (those belong to token text). That's
|
|
21
|
+
// why driving `ts.createScanner` directly without parser context is
|
|
22
|
+
// unsafe (regex / template `${}` interpolation get misclassified) and
|
|
23
|
+
// why the AST-driven walk stays correct.
|
|
24
|
+
//
|
|
25
|
+
// Multiple ignore plugins (one per directive form) share the result via
|
|
26
|
+
// a per-SourceFile WeakMap so the second / third callers in a pass pay
|
|
27
|
+
// one map lookup instead of another scan.
|
|
10
28
|
const sharedFileComments = new WeakMap();
|
|
11
|
-
function getFileComments(file) {
|
|
29
|
+
function getFileComments(ts, file) {
|
|
12
30
|
let comments = sharedFileComments.get(file);
|
|
13
31
|
if (!comments) {
|
|
14
32
|
comments = [];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
const fullText = file.text;
|
|
34
|
+
const SK = ts.SyntaxKind;
|
|
35
|
+
const notJsx = file.languageVariant !== ts.LanguageVariant.JSX;
|
|
36
|
+
// Iterative DFS: token leaves trigger trivia scans. Single shared
|
|
37
|
+
// callback object instead of allocating a closure per
|
|
38
|
+
// forEachLeading/Trailing call.
|
|
39
|
+
//
|
|
40
|
+
// CRITICAL: TS's `forEachLeading/TrailingCommentRange` stops
|
|
41
|
+
// iterating as soon as the callback returns a truthy value
|
|
42
|
+
// (matches the documented `forEachX` contract — return non-
|
|
43
|
+
// undefined to short-circuit). The callback body MUST NOT
|
|
44
|
+
// implicit-return `array.push(...)` (returns length → truthy →
|
|
45
|
+
// only the first comment per token gets collected).
|
|
46
|
+
const collected = comments;
|
|
47
|
+
const onComment = (pos, end) => {
|
|
48
|
+
const start = pos + 2; // strip `//` or `/*`
|
|
49
|
+
collected.push({
|
|
18
50
|
pos: start,
|
|
19
51
|
end,
|
|
20
52
|
text: fullText.substring(start, end),
|
|
21
53
|
});
|
|
22
|
-
|
|
54
|
+
// no return → undefined → continue iteration
|
|
55
|
+
};
|
|
56
|
+
// `getChildren` array per visit creates the most allocation
|
|
57
|
+
// pressure; we still need it because `forEachChild` skips token
|
|
58
|
+
// kinds. Iterative stack avoids the recursive overhead.
|
|
59
|
+
let stack = [file];
|
|
60
|
+
while (stack.length > 0) {
|
|
61
|
+
const node = stack.pop();
|
|
62
|
+
if (ts.isTokenKind(node.kind)) {
|
|
63
|
+
if (node.pos === node.end) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (node.kind !== SK.JsxText) {
|
|
67
|
+
// Skip the shebang at file position 0; otherwise
|
|
68
|
+
// `forEachLeadingCommentRange` would re-emit it as a
|
|
69
|
+
// line comment.
|
|
70
|
+
const scanFrom = node.pos === 0
|
|
71
|
+
? (ts.getShebang(fullText) ?? '').length
|
|
72
|
+
: node.pos;
|
|
73
|
+
ts.forEachLeadingCommentRange(fullText, scanFrom, onComment);
|
|
74
|
+
}
|
|
75
|
+
if (notJsx || canHaveTrailingTrivia(ts, node)) {
|
|
76
|
+
ts.forEachTrailingCommentRange(fullText, node.end, onComment);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const children = node.getChildren(file);
|
|
81
|
+
// Push in reverse so DFS order matches source order.
|
|
82
|
+
for (let i = children.length - 1; i >= 0; --i) {
|
|
83
|
+
stack.push(children[i]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
23
86
|
sharedFileComments.set(file, comments);
|
|
24
87
|
}
|
|
25
88
|
return comments;
|
|
26
89
|
}
|
|
90
|
+
function canHaveTrailingTrivia(ts, token) {
|
|
91
|
+
const SK = ts.SyntaxKind;
|
|
92
|
+
switch (token.kind) {
|
|
93
|
+
case SK.CloseBraceToken:
|
|
94
|
+
// `}` of a JsxExpression inside a JSX element: no trailing
|
|
95
|
+
// trivia (the next character is JsxText, not a comment).
|
|
96
|
+
return token.parent.kind !== SK.JsxExpression
|
|
97
|
+
|| !isJsxElementOrFragment(ts, token.parent.parent);
|
|
98
|
+
case SK.GreaterThanToken:
|
|
99
|
+
switch (token.parent.kind) {
|
|
100
|
+
case SK.JsxClosingElement:
|
|
101
|
+
case SK.JsxClosingFragment:
|
|
102
|
+
return !isJsxElementOrFragment(ts, token.parent.parent.parent);
|
|
103
|
+
case SK.JsxOpeningElement:
|
|
104
|
+
// Type-args list keeps trailing trivia; the `>` that
|
|
105
|
+
// closes the opening tag itself does not (next is
|
|
106
|
+
// JsxText).
|
|
107
|
+
return token.end !== token.parent.end;
|
|
108
|
+
case SK.JsxOpeningFragment:
|
|
109
|
+
return false;
|
|
110
|
+
case SK.JsxSelfClosingElement:
|
|
111
|
+
return token.end !== token.parent.end
|
|
112
|
+
|| !isJsxElementOrFragment(ts, token.parent.parent);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
function isJsxElementOrFragment(ts, node) {
|
|
118
|
+
const SK = ts.SyntaxKind;
|
|
119
|
+
return node.kind === SK.JsxElement || node.kind === SK.JsxFragment;
|
|
120
|
+
}
|
|
27
121
|
function create(cmdOption, reportsUnusedComments) {
|
|
28
122
|
const mode = typeof cmdOption === 'string' ? 'singleLine' : 'multiLine';
|
|
29
123
|
const [cmd, endCmd] = Array.isArray(cmdOption) ? cmdOption : [cmdOption, undefined];
|
|
30
124
|
const cmdText = cmd.replace(/\?/g, '');
|
|
31
|
-
|
|
125
|
+
// Rule IDs in the wild: bare (`no-shadow`), plugin-prefixed
|
|
126
|
+
// (`regexp/no-foo`), or scoped (`@typescript-eslint/no-foo`,
|
|
127
|
+
// `@scope/plugin/rule`). The previous `\w\S*` required a word char
|
|
128
|
+
// first — `@` failed, so every scoped disable comment fell into the
|
|
129
|
+
// `comments.get(undefined)` "disable all" bucket. Stacking multiple
|
|
130
|
+
// scoped disables then collapsed them into duplicates, only the first
|
|
131
|
+
// got marked used by an incoming error, the rest were reported as
|
|
132
|
+
// unused-comment FPs (real Astro repro: stacked `@typescript-eslint/...`
|
|
133
|
+
// block disables on `extendables.ts`).
|
|
134
|
+
//
|
|
135
|
+
// Match: leading scope segment `@xxx/`, then a name segment that
|
|
136
|
+
// starts with letter/underscore (skip `--` description delimiter),
|
|
137
|
+
// allowing `\w / . -` thereafter.
|
|
138
|
+
const ruleIdPattern = '(?:@[\\w-]+\\/)?[A-Za-z_][\\w/.-]*';
|
|
139
|
+
// Capture EVERYTHING after the command word as `tail`. The previous
|
|
140
|
+
// `(?<ruleId>...)?([ \\t]+[^\\r\\n]*)?` form captured a single rule
|
|
141
|
+
// id and let the trailing-text group eat the rest — but `,` is not
|
|
142
|
+
// in `[ \\t]+`, so for `eslint-disable rule1, rule2` the optional
|
|
143
|
+
// ruleId group backtracked to nothing, the trailing-text group then
|
|
144
|
+
// consumed `rule1, rule2 */`, and the result was treated as a bare
|
|
145
|
+
// `eslint-disable` (= disable ALL). Now we capture the whole tail
|
|
146
|
+
// and split it ourselves below to extract the rule list.
|
|
147
|
+
//
|
|
148
|
+
// `cmd` MUST be followed by whitespace, `*/`, or end of comment —
|
|
149
|
+
// not by another word/`-` char. Otherwise `eslint-disable` would
|
|
150
|
+
// also match `eslint-disable-line ...` (treating `-line ...` as a
|
|
151
|
+
// malformed rule list and falling back to disable-all). The
|
|
152
|
+
// `(?:\\s+(?<tail>...))?` form requires whitespace if anything
|
|
153
|
+
// follows.
|
|
32
154
|
const header = '^\\s*';
|
|
33
|
-
const
|
|
34
|
-
const reg = new RegExp(`${header}${cmd}${
|
|
35
|
-
const endReg = endCmd ? new RegExp(`${header}${endCmd}${
|
|
155
|
+
const tail = '(?:\\s+(?<tail>[^]*?))?(?:\\*\\/)?\\s*$';
|
|
156
|
+
const reg = new RegExp(`${header}${cmd}${tail}`);
|
|
157
|
+
const endReg = endCmd ? new RegExp(`${header}${endCmd}${tail}`) : undefined;
|
|
158
|
+
// `tail` parsing: split by `--` (description marker), keep left side,
|
|
159
|
+
// split by `,`, trim each, drop blanks, validate against ruleIdPattern.
|
|
160
|
+
// Empty result = disable-all (matches ESLint semantics).
|
|
161
|
+
const ruleIdRegExp = new RegExp(`^${ruleIdPattern}$`);
|
|
162
|
+
function parseRuleList(rawTail) {
|
|
163
|
+
if (!rawTail)
|
|
164
|
+
return undefined; // bare command — disable all
|
|
165
|
+
const beforeDescription = rawTail.split(/\s--\s/)[0];
|
|
166
|
+
const parts = beforeDescription.split(',').map(s => s.trim()).filter(Boolean);
|
|
167
|
+
if (parts.length === 0)
|
|
168
|
+
return undefined; // disable all
|
|
169
|
+
const valid = parts.filter(p => ruleIdRegExp.test(p));
|
|
170
|
+
// If any part is malformed (e.g. trailing `*/` snuck in), be
|
|
171
|
+
// conservative and treat the whole comment as disable-all rather
|
|
172
|
+
// than silently dropping rules. ESLint surfaces an error here;
|
|
173
|
+
// for our purposes mirroring the legacy "disable all on parse
|
|
174
|
+
// failure" preserves behaviour for malformed comments.
|
|
175
|
+
if (valid.length !== parts.length)
|
|
176
|
+
return undefined;
|
|
177
|
+
return valid;
|
|
178
|
+
}
|
|
36
179
|
const completeReg1 = /^\s*\/\/(\s*)([\S]*)?$/;
|
|
37
180
|
const completeReg2 = new RegExp(`//\\s*${cmd}(\\S*)?$`);
|
|
38
181
|
return ({ typescript: ts, languageService }) => {
|
|
@@ -135,15 +278,18 @@ function create(cmdOption, reportsUnusedComments) {
|
|
|
135
278
|
return results;
|
|
136
279
|
}
|
|
137
280
|
const comments = new Map();
|
|
138
|
-
for (const c of getFileComments(file)) {
|
|
281
|
+
for (const c of getFileComments(ts, file)) {
|
|
139
282
|
const startComment = c.text.match(reg);
|
|
140
283
|
if (startComment?.index !== undefined) {
|
|
141
284
|
const index = startComment.index + c.pos;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
285
|
+
const ruleList = parseRuleList(startComment.groups?.tail);
|
|
286
|
+
// `undefined` = bare command (disable all). Else
|
|
287
|
+
// register one entry PER rule so comma-separated
|
|
288
|
+
// disables don't collapse into the disable-all
|
|
289
|
+
// bucket (the bug: `eslint-disable a, b` used
|
|
290
|
+
// to silently disable everything because the old
|
|
291
|
+
// regex backtracked to ruleId=undefined).
|
|
292
|
+
const ruleKeys = ruleList ?? [undefined];
|
|
147
293
|
const line = file.getLineAndCharacterOfPosition(index).line;
|
|
148
294
|
let startLine = line;
|
|
149
295
|
if (mode === 'singleLine') {
|
|
@@ -152,23 +298,34 @@ function create(cmdOption, reportsUnusedComments) {
|
|
|
152
298
|
startLine = line + 1; // If the comment is at the start of the line, the error is in the next line
|
|
153
299
|
}
|
|
154
300
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
301
|
+
const commentRange = [
|
|
302
|
+
index - 2,
|
|
303
|
+
index + startComment[0].length,
|
|
304
|
+
];
|
|
305
|
+
for (const ruleId of ruleKeys) {
|
|
306
|
+
if (!comments.has(ruleId)) {
|
|
307
|
+
comments.set(ruleId, []);
|
|
308
|
+
}
|
|
309
|
+
// Per-rule state object: a paired `eslint-enable
|
|
310
|
+
// rule1` mutates `.endLine` on the matching
|
|
311
|
+
// rule's last entry. Sharing one state across
|
|
312
|
+
// `[rule1, rule2]` would cause the enable to
|
|
313
|
+
// also close rule2's window.
|
|
314
|
+
comments.get(ruleId).push({ commentRange, startLine });
|
|
315
|
+
}
|
|
162
316
|
}
|
|
163
317
|
else if (endReg) {
|
|
164
318
|
const endComment = c.text.match(endReg);
|
|
165
319
|
if (endComment?.index !== undefined) {
|
|
166
320
|
const index = endComment.index + c.pos;
|
|
167
321
|
const prevLine = file.getLineAndCharacterOfPosition(index).line;
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
disabledLines
|
|
322
|
+
const endRuleList = parseRuleList(endComment.groups?.tail);
|
|
323
|
+
const endRuleKeys = endRuleList ?? [undefined];
|
|
324
|
+
for (const ruleId of endRuleKeys) {
|
|
325
|
+
const disabledLines = comments.get(ruleId);
|
|
326
|
+
if (disabledLines) {
|
|
327
|
+
disabledLines[disabledLines.length - 1].endLine = prevLine;
|
|
328
|
+
}
|
|
172
329
|
}
|
|
173
330
|
}
|
|
174
331
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsslint/config",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.6.0"
|
|
@@ -18,12 +18,11 @@
|
|
|
18
18
|
"directory": "packages/config"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@tsslint/types": "3.1.
|
|
22
|
-
"minimatch": "^10.0.1"
|
|
23
|
-
"ts-api-utils": "^2.0.0"
|
|
21
|
+
"@tsslint/types": "3.1.2",
|
|
22
|
+
"minimatch": "^10.0.1"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
|
-
"@tsslint/compat-eslint": "3.1.
|
|
25
|
+
"@tsslint/compat-eslint": "3.1.2",
|
|
27
26
|
"tslint": "^6.1.3"
|
|
28
27
|
},
|
|
29
28
|
"peerDependencies": {
|
|
@@ -38,5 +37,5 @@
|
|
|
38
37
|
"optional": true
|
|
39
38
|
}
|
|
40
39
|
},
|
|
41
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "e6fb08d18e9f4feb2b0191199cecce4bdf3c4a29"
|
|
42
41
|
}
|