@tsslint/core 1.4.4 → 1.4.6

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.
Files changed (3) hide show
  1. package/index.d.ts +11 -7
  2. package/index.js +156 -164
  3. package/package.json +3 -3
package/index.d.ts CHANGED
@@ -1,22 +1,26 @@
1
1
  export * from './lib/build';
2
2
  export * from './lib/watch';
3
- import type { Config, ProjectContext, Rules } from '@tsslint/types';
3
+ import type { Config, ProjectContext, Rule, Rules } from '@tsslint/types';
4
4
  import type * as ts from 'typescript';
5
5
  export type FileLintCache = [
6
6
  mtime: number,
7
- ruleFixes: Record<string, number>,
8
- result: Record<string, ts.DiagnosticWithLocation[]>,
7
+ lintResult: Record<string, [
8
+ hasFix: boolean,
9
+ diagnostics: ts.DiagnosticWithLocation[]
10
+ ]>,
9
11
  minimatchResult: Record<string, boolean>
10
12
  ];
11
13
  export type Linter = ReturnType<typeof createLinter>;
12
- export declare function createLinter(ctx: ProjectContext, config: Config | Config[], mode: 'cli' | 'typescript-plugin'): {
14
+ export declare function createLinter(ctx: ProjectContext, config: Config | Config[], mode: 'cli' | 'typescript-plugin', syntaxOnlyLanguageService?: ts.LanguageService & {
15
+ getNonBoundSourceFile?(fileName: string): ts.SourceFile;
16
+ }): {
13
17
  lint(fileName: string, cache?: FileLintCache): ts.DiagnosticWithLocation[];
14
18
  hasCodeFixes(fileName: string): boolean;
15
- getCodeFixes(fileName: string, start: number, end: number, diagnostics?: ts.Diagnostic[], minimatchCache?: FileLintCache[3]): ts.CodeFixAction[];
19
+ getCodeFixes(fileName: string, start: number, end: number, diagnostics?: ts.Diagnostic[], minimatchCache?: FileLintCache[2]): ts.CodeFixAction[];
16
20
  getRefactors(fileName: string, start: number, end: number): ts.RefactorActionInfo[];
17
21
  getRefactorEdits(fileName: string, actionName: string): ts.FileTextChanges[] | undefined;
18
- getRules: (fileName: string, minimatchCache: undefined | FileLintCache[3]) => Rules;
19
- getConfigs: (fileName: string, minimatchCache: undefined | FileLintCache[3]) => {
22
+ getRules: (fileName: string, minimatchCache: undefined | FileLintCache[2]) => Record<string, Rule>;
23
+ getConfigs: (fileName: string, minimatchCache: undefined | FileLintCache[2]) => {
20
24
  include: string[];
21
25
  exclude: string[];
22
26
  rules: Rules;
package/index.js CHANGED
@@ -23,25 +23,11 @@ __exportStar(require("./lib/watch"), exports);
23
23
  const ErrorStackParser = require("error-stack-parser");
24
24
  const path = require("path");
25
25
  const minimatch = require("minimatch");
26
- const typeAwareModeChange = new Error('enable type-aware mode');
27
- function createLinter(ctx, config, mode) {
28
- let languageServiceUsage = 0;
26
+ function createLinter(ctx, config, mode, syntaxOnlyLanguageService) {
29
27
  const ts = ctx.typescript;
30
- const languageService = new Proxy(ctx.languageService, {
31
- get(target, key, receiver) {
32
- if (!languageServiceUsage) {
33
- languageServiceUsage++;
34
- throw typeAwareModeChange;
35
- }
36
- languageServiceUsage++;
37
- return Reflect.get(target, key, receiver);
38
- },
39
- });
40
28
  const fileRules = new Map();
41
29
  const fileConfigs = new Map();
42
- const fileFixes = new Map();
43
- const fileRefactors = new Map();
44
- const snapshot2SourceFile = new WeakMap();
30
+ const lintResults = new Map();
45
31
  const basePath = path.dirname(ctx.configFile);
46
32
  const configs = (Array.isArray(config) ? config : [config])
47
33
  .map(config => ({
@@ -51,106 +37,123 @@ function createLinter(ctx, config, mode) {
51
37
  plugins: (config.plugins ?? []).map(plugin => plugin(ctx)),
52
38
  }));
53
39
  const normalizedPath = new Map();
40
+ const rule2Mode = new Map();
41
+ const getNonBoundSourceFile = syntaxOnlyLanguageService?.getNonBoundSourceFile;
42
+ let shouldEnableTypeAware = false;
54
43
  return {
55
44
  lint(fileName, cache) {
56
- let diagnostics = [];
57
45
  let currentRuleId;
58
- let currentIssues = 0;
59
- let currentFixes = 0;
60
- let currentRefactors = 0;
61
- let currentRuleLanguageServiceUsage = 0;
62
- let sourceFile;
63
- const rules = getFileRules(fileName, cache?.[3]);
64
- const rulesContext = {
65
- ...ctx,
66
- languageService,
67
- get sourceFile() {
68
- return sourceFile ??= getSourceFile(fileName);
69
- },
70
- reportError,
71
- reportWarning,
72
- reportSuggestion,
73
- };
46
+ let shouldRetry = false;
47
+ const rules = getFileRules(fileName, cache?.[2]);
48
+ const typeAwareMode = !getNonBoundSourceFile
49
+ || shouldEnableTypeAware && !Object.keys(rules).some(ruleId => !rule2Mode.has(ruleId));
50
+ const rulesContext = typeAwareMode
51
+ ? {
52
+ ...ctx,
53
+ sourceFile: ctx.languageService.getProgram().getSourceFile(fileName),
54
+ reportError,
55
+ reportWarning,
56
+ reportSuggestion,
57
+ }
58
+ : {
59
+ ...ctx,
60
+ languageService: syntaxOnlyLanguageService,
61
+ sourceFile: getNonBoundSourceFile(fileName),
62
+ reportError,
63
+ reportWarning,
64
+ reportSuggestion,
65
+ };
74
66
  const token = ctx.languageServiceHost.getCancellationToken?.();
75
- const fixes = getFileFixes(fileName);
76
- const refactors = getFileRefactors(fileName);
77
- const configs = getFileConfigs(fileName, cache?.[3]);
78
- fixes.clear();
79
- refactors.length = 0;
80
- if (!runRules(rules)) {
81
- return this.lint(fileName, cache);
82
- }
83
- for (const { plugins } of configs) {
84
- for (const { resolveDiagnostics } of plugins) {
85
- if (resolveDiagnostics) {
86
- diagnostics = resolveDiagnostics(rulesContext.sourceFile, diagnostics);
87
- }
67
+ const configs = getFileConfigs(fileName, cache?.[2]);
68
+ lintResults.set(fileName, [rulesContext.sourceFile, new Map(), []]);
69
+ const lintResult = lintResults.get(fileName);
70
+ for (const [ruleId, rule] of Object.entries(rules)) {
71
+ if (token?.isCancellationRequested()) {
72
+ break;
88
73
  }
89
- }
90
- // Remove fixes and refactors that removed by resolveDiagnostics
91
- const diagnosticSet = new Set(diagnostics);
92
- for (const diagnostic of [...fixes.keys()]) {
93
- if (!diagnosticSet.has(diagnostic)) {
94
- fixes.delete(diagnostic);
74
+ currentRuleId = ruleId;
75
+ const ruleCache = cache?.[1][currentRuleId];
76
+ if (ruleCache) {
77
+ let lintResult = lintResults.get(fileName);
78
+ if (!lintResult) {
79
+ lintResults.set(fileName, lintResult = [rulesContext.sourceFile, new Map(), []]);
80
+ }
81
+ for (const cacheDiagnostic of ruleCache[1]) {
82
+ lintResult[1].set({
83
+ ...cacheDiagnostic,
84
+ file: rulesContext.sourceFile,
85
+ relatedInformation: cacheDiagnostic.relatedInformation?.map(info => ({
86
+ ...info,
87
+ file: info.file ? syntaxOnlyLanguageService.getNonBoundSourceFile(info.file.fileName) : undefined,
88
+ })),
89
+ }, []);
90
+ }
91
+ if (!typeAwareMode) {
92
+ rule2Mode.set(currentRuleId, false);
93
+ }
94
+ continue;
95
95
  }
96
- }
97
- fileRefactors.set(fileName, refactors.filter(refactor => diagnosticSet.has(refactor.diagnostic)));
98
- return diagnostics;
99
- function runRules(rules, paths = []) {
100
- for (const [path, rule] of Object.entries(rules)) {
101
- if (token?.isCancellationRequested()) {
102
- break;
96
+ try {
97
+ rule(rulesContext);
98
+ if (!typeAwareMode) {
99
+ rule2Mode.set(currentRuleId, false);
103
100
  }
104
- if (typeof rule === 'object') {
105
- if (!runRules(rule, [...paths, path])) {
106
- return false;
107
- }
108
- continue;
101
+ }
102
+ catch (err) {
103
+ if (!typeAwareMode) {
104
+ // console.log(`Rule "${currentRuleId}" is type aware.`);
105
+ rule2Mode.set(currentRuleId, true);
106
+ shouldRetry = true;
109
107
  }
110
- currentRuleLanguageServiceUsage = languageServiceUsage;
111
- currentRuleId = [...paths, path].join('/');
112
- currentIssues = 0;
113
- currentFixes = 0;
114
- currentRefactors = 0;
115
- if (cache) {
116
- const ruleCache = cache[2][currentRuleId];
117
- if (ruleCache) {
118
- diagnostics.push(...ruleCache.map(data => ({
119
- ...data,
120
- file: rulesContext.sourceFile,
121
- relatedInformation: data.relatedInformation?.map(info => ({
122
- ...info,
123
- file: info.file ? getSourceFile(info.file.fileName) : undefined,
124
- })),
125
- })));
126
- continue;
127
- }
108
+ else if (err instanceof Error) {
109
+ report(ts.DiagnosticCategory.Error, err.stack ?? err.message, 0, 0, 0, err);
128
110
  }
129
- try {
130
- rule(rulesContext);
111
+ else {
112
+ report(ts.DiagnosticCategory.Error, String(err), 0, 0, false);
131
113
  }
132
- catch (err) {
133
- if (err === typeAwareModeChange) {
134
- // logger?.log.message(`Type-aware mode enabled by ${currentRuleId} rule.`);
135
- return false;
136
- }
137
- else if (err instanceof Error) {
138
- report(ts.DiagnosticCategory.Error, err.stack ?? err.message, 0, 0, 0, err);
139
- }
140
- else {
141
- report(ts.DiagnosticCategory.Error, String(err), 0, 0, false);
114
+ }
115
+ if (cache && !rule2Mode.get(currentRuleId)) {
116
+ cache[1][currentRuleId] ??= [false, []];
117
+ for (const [_, fixes] of lintResult[1]) {
118
+ if (fixes.length) {
119
+ cache[1][currentRuleId][0] = true;
120
+ break;
142
121
  }
143
122
  }
144
- if (cache) {
145
- if (currentRuleLanguageServiceUsage === languageServiceUsage) {
146
- cache[1][currentRuleId] = currentFixes;
147
- cache[2][currentRuleId] ??= [];
123
+ }
124
+ }
125
+ if (shouldRetry) {
126
+ // Retry
127
+ shouldEnableTypeAware = true;
128
+ return this.lint(fileName, cache);
129
+ }
130
+ let diagnostics = [...lintResult[1].keys()];
131
+ try {
132
+ for (const { plugins } of configs) {
133
+ for (const { resolveDiagnostics } of plugins) {
134
+ if (resolveDiagnostics) {
135
+ diagnostics = resolveDiagnostics(rulesContext.sourceFile, diagnostics);
148
136
  }
149
137
  }
150
138
  }
151
- return true;
152
139
  }
153
- ;
140
+ catch (error) {
141
+ if (!typeAwareMode) {
142
+ // Retry
143
+ shouldEnableTypeAware = true;
144
+ return this.lint(fileName, cache);
145
+ }
146
+ throw error;
147
+ }
148
+ // Remove fixes and refactors that removed by resolveDiagnostics
149
+ const diagnosticSet = new Set(diagnostics);
150
+ for (const diagnostic of [...lintResult[1].keys()]) {
151
+ if (!diagnosticSet.has(diagnostic)) {
152
+ lintResult[1].delete(diagnostic);
153
+ }
154
+ }
155
+ lintResult[2] = lintResult[2].filter(refactor => diagnosticSet.has(refactor.diagnostic));
156
+ return diagnostics;
154
157
  function reportError(message, start, end, stackOffset) {
155
158
  return report(ts.DiagnosticCategory.Error, message, start, end, stackOffset);
156
159
  }
@@ -171,10 +174,9 @@ function createLinter(ctx, config, mode) {
171
174
  source: 'tsslint',
172
175
  relatedInformation: [],
173
176
  };
174
- const cacheable = currentRuleLanguageServiceUsage === languageServiceUsage;
175
- if (cache && cacheable) {
176
- cache[2][currentRuleId] ??= [];
177
- cache[2][currentRuleId].push({
177
+ if (cache && !rule2Mode.get(currentRuleId)) {
178
+ cache[1][currentRuleId] ??= [false, []];
179
+ cache[1][currentRuleId][1].push({
178
180
  ...error,
179
181
  file: undefined,
180
182
  relatedInformation: error.relatedInformation?.map(info => ({
@@ -190,9 +192,14 @@ function createLinter(ctx, config, mode) {
190
192
  error.relatedInformation.push(relatedInfo);
191
193
  }
192
194
  }
193
- fixes.set(error, []);
194
- diagnostics.push(error);
195
- currentIssues++;
195
+ let lintResult = lintResults.get(fileName);
196
+ if (!lintResult) {
197
+ lintResults.set(fileName, lintResult = [rulesContext.sourceFile, new Map(), []]);
198
+ }
199
+ const diagnostic2Fixes = lintResult[1];
200
+ const refactors = lintResult[2];
201
+ diagnostic2Fixes.set(error, []);
202
+ const fixes = diagnostic2Fixes.get(error);
196
203
  return {
197
204
  withDeprecated() {
198
205
  error.reportsDeprecated = true;
@@ -203,12 +210,10 @@ function createLinter(ctx, config, mode) {
203
210
  return this;
204
211
  },
205
212
  withFix(title, getEdits) {
206
- currentFixes++;
207
- fixes.get(error).push(({ title, getEdits }));
213
+ fixes.push(({ title, getEdits }));
208
214
  return this;
209
215
  },
210
216
  withRefactor(title, getEdits) {
211
- currentRefactors++;
212
217
  refactors.push(({
213
218
  diagnostic: error,
214
219
  title,
@@ -220,19 +225,26 @@ function createLinter(ctx, config, mode) {
220
225
  }
221
226
  },
222
227
  hasCodeFixes(fileName) {
223
- const fixesMap = getFileFixes(fileName);
224
- for (const [_diagnostic, actions] of fixesMap) {
225
- if (actions.length) {
228
+ const lintResult = lintResults.get(fileName);
229
+ if (!lintResult) {
230
+ return false;
231
+ }
232
+ for (const [_, fixes] of lintResult[1]) {
233
+ if (fixes.length) {
226
234
  return true;
227
235
  }
228
236
  }
229
237
  return false;
230
238
  },
231
239
  getCodeFixes(fileName, start, end, diagnostics, minimatchCache) {
240
+ const lintResult = lintResults.get(fileName);
241
+ if (!lintResult) {
242
+ return [];
243
+ }
244
+ const sourceFile = lintResult[0];
232
245
  const configs = getFileConfigs(fileName, minimatchCache);
233
- const fixesMap = getFileFixes(fileName);
234
246
  const result = [];
235
- for (const [diagnostic, actions] of fixesMap) {
247
+ for (const [diagnostic, actions] of lintResult[1]) {
236
248
  if (diagnostics?.length && !diagnostics.includes(diagnostic)) {
237
249
  continue;
238
250
  }
@@ -255,7 +267,7 @@ function createLinter(ctx, config, mode) {
255
267
  for (const { plugins } of configs) {
256
268
  for (const { resolveCodeFixes } of plugins) {
257
269
  if (resolveCodeFixes) {
258
- codeFixes = resolveCodeFixes(getSourceFile(fileName), diagnostic, codeFixes);
270
+ codeFixes = resolveCodeFixes(sourceFile, diagnostic, codeFixes);
259
271
  }
260
272
  }
261
273
  }
@@ -265,10 +277,13 @@ function createLinter(ctx, config, mode) {
265
277
  return result;
266
278
  },
267
279
  getRefactors(fileName, start, end) {
268
- const refactors = getFileRefactors(fileName);
280
+ const lintResult = lintResults.get(fileName);
281
+ if (!lintResult) {
282
+ return [];
283
+ }
269
284
  const result = [];
270
- for (let i = 0; i < refactors.length; i++) {
271
- const refactor = refactors[i];
285
+ for (let i = 0; i < lintResult[2].length; i++) {
286
+ const refactor = lintResult[2][i];
272
287
  const diagStart = refactor.diagnostic.start;
273
288
  const diagEnd = diagStart + refactor.diagnostic.length;
274
289
  if ((diagStart >= start && diagStart <= end) ||
@@ -286,9 +301,12 @@ function createLinter(ctx, config, mode) {
286
301
  },
287
302
  getRefactorEdits(fileName, actionName) {
288
303
  if (actionName.startsWith('tsslint:')) {
304
+ const lintResult = lintResults.get(fileName);
305
+ if (!lintResult) {
306
+ return [];
307
+ }
289
308
  const index = actionName.slice('tsslint:'.length);
290
- const actions = getFileRefactors(fileName);
291
- const refactor = actions[Number(index)];
309
+ const refactor = lintResult[2][Number(index)];
292
310
  if (refactor) {
293
311
  return refactor.getEdits();
294
312
  }
@@ -297,47 +315,33 @@ function createLinter(ctx, config, mode) {
297
315
  getRules: getFileRules,
298
316
  getConfigs: getFileConfigs,
299
317
  };
300
- function getSourceFile(fileName) {
301
- if (languageServiceUsage) {
302
- const sourceFile = ctx.languageService.getProgram().getSourceFile(fileName);
303
- if (sourceFile) {
304
- return sourceFile;
305
- }
306
- }
307
- else {
308
- const snapshot = ctx.languageServiceHost.getScriptSnapshot(fileName);
309
- if (snapshot) {
310
- if (!snapshot2SourceFile.has(snapshot)) {
311
- const sourceFile = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.ESNext, true);
312
- snapshot2SourceFile.set(snapshot, sourceFile);
313
- return sourceFile;
314
- }
315
- return snapshot2SourceFile.get(snapshot);
316
- }
317
- }
318
- throw new Error('No source file');
319
- }
320
318
  function getFileRules(fileName, minimatchCache) {
321
- let result = fileRules.get(fileName);
322
- if (!result) {
323
- result = {};
319
+ let rules = fileRules.get(fileName);
320
+ if (!rules) {
321
+ rules = {};
324
322
  const configs = getFileConfigs(fileName, minimatchCache);
325
- for (const { rules } of configs) {
326
- result = {
327
- ...result,
328
- ...rules,
329
- };
323
+ for (const config of configs) {
324
+ collectRules(rules, config.rules, []);
330
325
  }
331
326
  for (const { plugins } of configs) {
332
327
  for (const { resolveRules } of plugins) {
333
328
  if (resolveRules) {
334
- result = resolveRules(fileName, result);
329
+ rules = resolveRules(fileName, rules);
335
330
  }
336
331
  }
337
332
  }
338
- fileRules.set(fileName, result);
333
+ fileRules.set(fileName, rules);
334
+ }
335
+ return rules;
336
+ }
337
+ function collectRules(record, rules, paths) {
338
+ for (const [path, rule] of Object.entries(rules)) {
339
+ if (typeof rule === 'object') {
340
+ collectRules(record, rule, [...paths, path]);
341
+ continue;
342
+ }
343
+ record[[...paths, path].join('/')] = rule;
339
344
  }
340
- return result;
341
345
  }
342
346
  function getFileConfigs(fileName, minimatchCache) {
343
347
  let result = fileConfigs.get(fileName);
@@ -372,18 +376,6 @@ function createLinter(ctx, config, mode) {
372
376
  }
373
377
  return result;
374
378
  }
375
- function getFileFixes(fileName) {
376
- if (!fileFixes.has(fileName)) {
377
- fileFixes.set(fileName, new Map());
378
- }
379
- return fileFixes.get(fileName);
380
- }
381
- function getFileRefactors(fileName) {
382
- if (!fileRefactors.has(fileName)) {
383
- fileRefactors.set(fileName, []);
384
- }
385
- return fileRefactors.get(fileName);
386
- }
387
379
  }
388
380
  const fsFiles = new Map();
389
381
  function createRelatedInformation(ts, err, stackOffset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsslint/core",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "**/*.js",
@@ -12,7 +12,7 @@
12
12
  "directory": "packages/core"
13
13
  },
14
14
  "dependencies": {
15
- "@tsslint/types": "1.4.4",
15
+ "@tsslint/types": "1.4.6",
16
16
  "error-stack-parser": "^2.1.4",
17
17
  "esbuild": ">=0.17.0",
18
18
  "minimatch": "^10.0.1"
@@ -23,5 +23,5 @@
23
23
  "scripts": {
24
24
  "postinstall": "node scripts/cleanCache.js"
25
25
  },
26
- "gitHead": "7e4401c6734fd2b8d09d9aa45584e83e3ecf9e48"
26
+ "gitHead": "9c5bc3471bb1c737144f90a1a70470975088e7fc"
27
27
  }