@tsslint/core 1.2.3 → 1.3.0

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/index.d.ts CHANGED
@@ -2,11 +2,18 @@ export * from './lib/build';
2
2
  export * from './lib/watch';
3
3
  import type { Config, ProjectContext } from '@tsslint/types';
4
4
  import type * as ts from 'typescript';
5
+ export type FileLintCache = [
6
+ mtime: number,
7
+ ruleFixes: Record<string, number>,
8
+ result: ts.DiagnosticWithLocation[],
9
+ resolvedResult: ts.DiagnosticWithLocation[],
10
+ minimatchResult: Record<string, boolean>
11
+ ];
5
12
  export type Linter = ReturnType<typeof createLinter>;
6
- export declare function createLinter(ctx: ProjectContext, config: Config | Config[], withStack: boolean): {
7
- lint(fileName: string): ts.DiagnosticWithLocation[];
13
+ export declare function createLinter(ctx: ProjectContext, config: Config | Config[], mode: 'cli' | 'typescript-plugin'): {
14
+ lint(fileName: string, cache?: FileLintCache): ts.DiagnosticWithLocation[];
8
15
  hasCodeFixes(fileName: string): boolean;
9
- getCodeFixes(fileName: string, start: number, end: number, diagnostics?: ts.Diagnostic[]): ts.CodeFixAction[];
16
+ getCodeFixes(fileName: string, start: number, end: number, diagnostics?: ts.Diagnostic[], cache?: FileLintCache): ts.CodeFixAction[];
10
17
  getRefactors(fileName: string, start: number, end: number): ts.RefactorActionInfo[];
11
18
  getRefactorEdits(fileName: string, actionName: string): ts.FileTextChanges[] | undefined;
12
19
  };
package/index.js CHANGED
@@ -22,8 +22,8 @@ __exportStar(require("./lib/watch"), exports);
22
22
  const ErrorStackParser = require("error-stack-parser");
23
23
  const path = require("path");
24
24
  const minimatch = require("minimatch");
25
- function createLinter(ctx, config, withStack) {
26
- if (withStack) {
25
+ function createLinter(ctx, config, mode) {
26
+ if (mode === 'typescript-plugin') {
27
27
  require('source-map-support').install({
28
28
  retrieveFile(path) {
29
29
  if (!path.endsWith('.js.map')) {
@@ -44,85 +44,168 @@ function createLinter(ctx, config, withStack) {
44
44
  },
45
45
  });
46
46
  }
47
+ let languageServiceUsage = mode === 'typescript-plugin' ? 1 : 0;
47
48
  const ts = ctx.typescript;
49
+ const languageService = new Proxy(ctx.languageService, {
50
+ get(target, key, receiver) {
51
+ if (!languageServiceUsage && debug) {
52
+ console.log('Type-aware mode enabled');
53
+ }
54
+ languageServiceUsage++;
55
+ return Reflect.get(target, key, receiver);
56
+ },
57
+ });
48
58
  const fileRules = new Map();
49
59
  const fileConfigs = new Map();
50
60
  const fileFixes = new Map();
51
61
  const fileRefactors = new Map();
52
62
  const sourceFiles = new Map();
63
+ const snapshot2SourceFile = new WeakMap();
53
64
  const basePath = path.dirname(ctx.configFile);
54
65
  const configs = (Array.isArray(config) ? config : [config])
55
66
  .map(config => ({
67
+ include: config.include ?? [],
68
+ exclude: config.exclude ?? [],
56
69
  rules: config.rules ?? {},
57
- includes: (config.include ?? []).map(include => {
58
- return ts.server.toNormalizedPath(path.resolve(basePath, include));
59
- }),
60
- excludes: (config.exclude ?? []).map(exclude => {
61
- return ts.server.toNormalizedPath(path.resolve(basePath, exclude));
62
- }),
63
70
  plugins: (config.plugins ?? []).map(plugin => plugin(ctx)),
64
71
  }));
72
+ const normalizedPath = new Map();
65
73
  const debug = (Array.isArray(config) ? config : [config]).some(config => config.debug);
66
74
  return {
67
- lint(fileName) {
68
- let diagnostics = [];
75
+ lint(fileName, cache) {
76
+ let cacheableDiagnostics = [];
77
+ let uncacheableDiagnostics = [];
69
78
  let debugInfo;
79
+ let currentRuleId;
80
+ let currentIssues = 0;
81
+ let currentFixes = 0;
82
+ let currentRefactors = 0;
83
+ let currentRuleLanguageServiceUsage = 0;
84
+ let sourceFile;
85
+ let hasUncacheResult = false;
70
86
  if (debug) {
71
87
  debugInfo = {
72
88
  category: ts.DiagnosticCategory.Message,
73
89
  code: 'debug',
74
90
  messageText: '- Config: ' + ctx.configFile + '\n',
75
- file: ctx.languageService.getProgram().getSourceFile(fileName),
91
+ file: getSourceFile(fileName),
76
92
  start: 0,
77
93
  length: 0,
78
94
  source: 'tsslint',
79
95
  relatedInformation: [],
80
96
  };
81
- diagnostics.push(debugInfo);
97
+ uncacheableDiagnostics.push(debugInfo);
82
98
  }
83
- const rules = getFileRules(fileName);
99
+ const rules = getFileRules(fileName, cache);
84
100
  if (!rules || !Object.keys(rules).length) {
85
101
  if (debugInfo) {
86
102
  debugInfo.messageText += '- Rules: ❌ (no rules)\n';
87
103
  }
88
- return diagnostics;
89
- }
90
- const sourceFile = ctx.languageService.getProgram()?.getSourceFile(fileName);
91
- if (!sourceFile) {
92
- throw new Error(`No source file found for ${fileName}`);
93
104
  }
105
+ const prevLanguageServiceUsage = languageServiceUsage;
94
106
  const rulesContext = {
95
107
  ...ctx,
96
- sourceFile,
108
+ languageService,
109
+ get sourceFile() {
110
+ return sourceFile ??= getSourceFile(fileName);
111
+ },
97
112
  reportError,
98
113
  reportWarning,
99
114
  reportSuggestion,
100
115
  };
101
116
  const token = ctx.languageServiceHost.getCancellationToken?.();
102
- const fixes = getFileFixes(sourceFile.fileName);
103
- const refactors = getFileRefactors(sourceFile.fileName);
104
- let currentRuleId;
105
- let currentIssues = 0;
106
- let currentFixes = 0;
107
- let currentRefactors = 0;
117
+ const fixes = getFileFixes(fileName);
118
+ const refactors = getFileRefactors(fileName);
119
+ const cachedRules = new Map();
120
+ if (cache) {
121
+ for (const ruleId in cache[1]) {
122
+ cachedRules.set(ruleId, cache[1][ruleId]);
123
+ }
124
+ }
108
125
  fixes.clear();
109
126
  refactors.length = 0;
110
127
  if (debugInfo) {
111
128
  debugInfo.messageText += '- Rules:\n';
112
129
  }
113
- const processRules = (rules, paths = []) => {
130
+ runRules(rules);
131
+ if (!!prevLanguageServiceUsage !== !!languageServiceUsage) {
132
+ return this.lint(fileName, cache);
133
+ }
134
+ const configs = getFileConfigs(fileName, cache);
135
+ if (cache) {
136
+ for (const [ruleId, fixes] of cachedRules) {
137
+ cache[1][ruleId] = fixes;
138
+ }
139
+ }
140
+ let diagnostics;
141
+ if (hasUncacheResult) {
142
+ diagnostics = [
143
+ ...(cacheableDiagnostics.length
144
+ ? cacheableDiagnostics
145
+ : (cache?.[2] ?? []).map(data => ({
146
+ ...data,
147
+ file: rulesContext.sourceFile,
148
+ relatedInformation: data.relatedInformation?.map(info => ({
149
+ ...info,
150
+ file: info.file ? getSourceFile(info.file.fileName) : undefined,
151
+ })),
152
+ }))),
153
+ ...uncacheableDiagnostics,
154
+ ];
155
+ for (const { plugins } of configs) {
156
+ for (const { resolveDiagnostics } of plugins) {
157
+ if (resolveDiagnostics) {
158
+ diagnostics = resolveDiagnostics(rulesContext.sourceFile, diagnostics);
159
+ }
160
+ }
161
+ }
162
+ if (cache) {
163
+ cache[3] = diagnostics.map(data => ({
164
+ ...data,
165
+ file: undefined,
166
+ relatedInformation: data.relatedInformation?.map(info => ({
167
+ ...info,
168
+ file: info.file ? { fileName: info.file.fileName } : undefined,
169
+ })),
170
+ }));
171
+ }
172
+ }
173
+ else {
174
+ diagnostics = (cache?.[3] ?? []).map(data => ({
175
+ ...data,
176
+ file: rulesContext.sourceFile,
177
+ relatedInformation: data.relatedInformation?.map(info => ({
178
+ ...info,
179
+ file: info.file ? getSourceFile(info.file.fileName) : undefined,
180
+ })),
181
+ }));
182
+ }
183
+ const diagnosticSet = new Set(diagnostics);
184
+ for (const diagnostic of [...fixes.keys()]) {
185
+ if (!diagnosticSet.has(diagnostic)) {
186
+ fixes.delete(diagnostic);
187
+ }
188
+ }
189
+ fileRefactors.set(fileName, refactors.filter(refactor => diagnosticSet.has(refactor.diagnostic)));
190
+ return diagnostics;
191
+ function runRules(rules, paths = []) {
114
192
  for (const [path, rule] of Object.entries(rules)) {
115
193
  if (token?.isCancellationRequested()) {
116
194
  break;
117
195
  }
118
196
  if (typeof rule === 'object') {
119
- processRules(rule, [...paths, path]);
197
+ runRules(rule, [...paths, path]);
120
198
  continue;
121
199
  }
200
+ currentRuleLanguageServiceUsage = languageServiceUsage;
122
201
  currentRuleId = [...paths, path].join('/');
123
202
  currentIssues = 0;
124
203
  currentFixes = 0;
125
204
  currentRefactors = 0;
205
+ if (cachedRules.has(currentRuleId)) {
206
+ continue;
207
+ }
208
+ hasUncacheResult = true;
126
209
  const start = Date.now();
127
210
  try {
128
211
  rule(rulesContext);
@@ -154,25 +237,12 @@ function createLinter(ctx, config, withStack) {
154
237
  debugInfo.messageText += ` - ${currentRuleId} (❌ ${err && typeof err === 'object' && 'stack' in err ? err.stack : String(err)}})\n`;
155
238
  }
156
239
  }
157
- }
158
- };
159
- processRules(rules);
160
- const configs = getFileConfigs(fileName);
161
- for (const { plugins } of configs) {
162
- for (const { resolveDiagnostics } of plugins) {
163
- if (resolveDiagnostics) {
164
- diagnostics = resolveDiagnostics(sourceFile.fileName, diagnostics);
240
+ if (cache && currentRuleLanguageServiceUsage === languageServiceUsage) {
241
+ cachedRules.set(currentRuleId, currentFixes);
165
242
  }
166
243
  }
167
244
  }
168
- const diagnosticSet = new Set(diagnostics);
169
- for (const diagnostic of [...fixes.keys()]) {
170
- if (!diagnosticSet.has(diagnostic)) {
171
- fixes.delete(diagnostic);
172
- }
173
- }
174
- fileRefactors.set(fileName, refactors.filter(refactor => diagnosticSet.has(refactor.diagnostic)));
175
- return diagnostics;
245
+ ;
176
246
  function reportError(message, start, end, traceOffset = 0) {
177
247
  return report(ts.DiagnosticCategory.Error, message, start, end, traceOffset);
178
248
  }
@@ -187,13 +257,24 @@ function createLinter(ctx, config, withStack) {
187
257
  category,
188
258
  code: currentRuleId,
189
259
  messageText: message,
190
- file: sourceFile,
260
+ file: rulesContext.sourceFile,
191
261
  start,
192
262
  length: end - start,
193
263
  source: 'tsslint',
194
264
  relatedInformation: [],
195
265
  };
196
- if (withStack) {
266
+ const cacheable = currentRuleLanguageServiceUsage === languageServiceUsage;
267
+ if (cache && cacheable) {
268
+ cache[2].push({
269
+ ...error,
270
+ file: undefined,
271
+ relatedInformation: error.relatedInformation?.map(info => ({
272
+ ...info,
273
+ file: info.file ? { fileName: info.file.fileName } : undefined,
274
+ })),
275
+ });
276
+ }
277
+ if (mode === 'typescript-plugin') {
197
278
  const stacks = traceOffset === false
198
279
  ? []
199
280
  : ErrorStackParser.parse(new Error());
@@ -204,7 +285,8 @@ function createLinter(ctx, config, withStack) {
204
285
  }
205
286
  }
206
287
  }
207
- diagnostics.push(error);
288
+ fixes.set(error, []);
289
+ (cacheable ? cacheableDiagnostics : uncacheableDiagnostics).push(error);
208
290
  currentIssues++;
209
291
  return {
210
292
  withDeprecated() {
@@ -217,9 +299,6 @@ function createLinter(ctx, config, withStack) {
217
299
  },
218
300
  withFix(title, getEdits) {
219
301
  currentFixes++;
220
- if (!fixes.has(error)) {
221
- fixes.set(error, []);
222
- }
223
302
  fixes.get(error).push(({ title, getEdits }));
224
303
  return this;
225
304
  },
@@ -281,8 +360,8 @@ function createLinter(ctx, config, withStack) {
281
360
  }
282
361
  return false;
283
362
  },
284
- getCodeFixes(fileName, start, end, diagnostics) {
285
- const configs = getFileConfigs(fileName);
363
+ getCodeFixes(fileName, start, end, diagnostics, cache) {
364
+ const configs = getFileConfigs(fileName, cache);
286
365
  const fixesMap = getFileFixes(fileName);
287
366
  const result = [];
288
367
  for (const [diagnostic, actions] of fixesMap) {
@@ -308,7 +387,7 @@ function createLinter(ctx, config, withStack) {
308
387
  for (const { plugins } of configs) {
309
388
  for (const { resolveCodeFixes } of plugins) {
310
389
  if (resolveCodeFixes) {
311
- codeFixes = resolveCodeFixes(fileName, diagnostic, codeFixes);
390
+ codeFixes = resolveCodeFixes(getSourceFile(fileName), diagnostic, codeFixes);
312
391
  }
313
392
  }
314
393
  }
@@ -348,11 +427,28 @@ function createLinter(ctx, config, withStack) {
348
427
  }
349
428
  },
350
429
  };
351
- function getFileRules(fileName) {
430
+ function getSourceFile(fileName) {
431
+ if (languageServiceUsage) {
432
+ return ctx.languageService.getProgram().getSourceFile(fileName);
433
+ }
434
+ else {
435
+ const snapshot = ctx.languageServiceHost.getScriptSnapshot(fileName);
436
+ if (snapshot) {
437
+ if (!snapshot2SourceFile.has(snapshot)) {
438
+ const sourceFile = ts.createSourceFile(fileName, snapshot.getText(0, snapshot.getLength()), ts.ScriptTarget.ESNext, true);
439
+ snapshot2SourceFile.set(snapshot, sourceFile);
440
+ return sourceFile;
441
+ }
442
+ return snapshot2SourceFile.get(snapshot);
443
+ }
444
+ }
445
+ throw new Error('No source file');
446
+ }
447
+ function getFileRules(fileName, cache) {
352
448
  let result = fileRules.get(fileName);
353
449
  if (!result) {
354
450
  result = {};
355
- const configs = getFileConfigs(fileName);
451
+ const configs = getFileConfigs(fileName, cache);
356
452
  for (const { rules } of configs) {
357
453
  result = {
358
454
  ...result,
@@ -370,19 +466,36 @@ function createLinter(ctx, config, withStack) {
370
466
  }
371
467
  return result;
372
468
  }
373
- function getFileConfigs(fileName) {
469
+ function getFileConfigs(fileName, cache) {
374
470
  let result = fileConfigs.get(fileName);
375
471
  if (!result) {
376
- result = configs.filter(({ includes, excludes }) => {
377
- if (excludes.some(pattern => minimatch.minimatch(fileName, pattern))) {
472
+ result = configs.filter(({ include, exclude }) => {
473
+ if (exclude.some(_minimatch)) {
378
474
  return false;
379
475
  }
380
- if (includes.length && !includes.some(pattern => minimatch.minimatch(fileName, pattern))) {
476
+ if (include.length && !include.some(_minimatch)) {
381
477
  return false;
382
478
  }
383
479
  return true;
384
480
  });
385
481
  fileConfigs.set(fileName, result);
482
+ function _minimatch(pattern) {
483
+ if (cache) {
484
+ if (pattern in cache[4]) {
485
+ return cache[4][pattern];
486
+ }
487
+ }
488
+ let normalized = normalizedPath.get(pattern);
489
+ if (!normalized) {
490
+ normalized = ts.server.toNormalizedPath(path.resolve(basePath, pattern));
491
+ normalizedPath.set(pattern, normalized);
492
+ }
493
+ const res = minimatch.minimatch(fileName, normalized);
494
+ if (cache) {
495
+ cache[4][pattern] = res;
496
+ }
497
+ return res;
498
+ }
386
499
  }
387
500
  return result;
388
501
  }
package/lib/cache.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { ProjectContext } from '@tsslint/types';
2
+ export declare function loadCache(configFilePath: string, createHash?: (path: string) => string): ProjectContext['cache'];
3
+ export declare function saveCache(configFilePath: string, cache: ProjectContext['cache'], createHash?: (path: string) => string): void;
4
+ export declare function getDotTsslintPath(configFilePath: string): string;
package/lib/cache.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadCache = loadCache;
4
+ exports.saveCache = saveCache;
5
+ exports.getDotTsslintPath = getDotTsslintPath;
6
+ const path = require("path");
7
+ const fs = require("fs");
8
+ function loadCache(configFilePath, createHash = btoa) {
9
+ const outDir = getDotTsslintPath(configFilePath);
10
+ const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '.cache.json';
11
+ const cacheFilePath = path.join(outDir, cacheFileName);
12
+ const cacheFileStat = fs.statSync(cacheFilePath);
13
+ const configFileStat = fs.statSync(configFilePath);
14
+ if (cacheFileStat.isFile() && cacheFileStat.mtimeMs > configFileStat.mtimeMs) {
15
+ try {
16
+ return require(cacheFilePath);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ return {};
23
+ }
24
+ function saveCache(configFilePath, cache, createHash = btoa) {
25
+ const outDir = getDotTsslintPath(configFilePath);
26
+ const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '.cache.json';
27
+ const cacheFilePath = path.join(outDir, cacheFileName);
28
+ fs.writeFileSync(cacheFilePath, JSON.stringify(cache));
29
+ }
30
+ function getDotTsslintPath(configFilePath) {
31
+ return path.resolve(configFilePath, '..', 'node_modules', '.tsslint');
32
+ }
33
+ //# sourceMappingURL=cache.js.map
package/lib/watch.d.ts CHANGED
@@ -12,3 +12,4 @@ export declare function watchConfigFile(configFilePath: string, onBuild: (config
12
12
  setup(build: esbuild.PluginBuild): void;
13
13
  }[];
14
14
  }>>;
15
+ export declare function getDotTsslintPath(configFilePath: string): string;
package/lib/watch.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.watchConfigFile = watchConfigFile;
4
+ exports.getDotTsslintPath = getDotTsslintPath;
4
5
  const esbuild = require("esbuild");
5
6
  const _path = require("path");
6
7
  const fs = require("fs");
@@ -8,7 +9,7 @@ const url = require("url");
8
9
  const ErrorStackParser = require("error-stack-parser");
9
10
  async function watchConfigFile(configFilePath, onBuild, watch = true, createHash = btoa, logger = console) {
10
11
  let start;
11
- const outDir = _path.resolve(configFilePath, '..', 'node_modules', '.tsslint');
12
+ const outDir = getDotTsslintPath(configFilePath);
12
13
  const outFileName = createHash(_path.relative(outDir, configFilePath)) + '.mjs';
13
14
  const outFile = _path.join(outDir, outFileName);
14
15
  const resultHandler = async (result) => {
@@ -135,4 +136,7 @@ async function watchConfigFile(configFilePath, onBuild, watch = true, createHash
135
136
  function isTsFile(path) {
136
137
  return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.cts') || path.endsWith('.mts');
137
138
  }
139
+ function getDotTsslintPath(configFilePath) {
140
+ return _path.resolve(configFilePath, '..', 'node_modules', '.tsslint');
141
+ }
138
142
  //# sourceMappingURL=watch.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsslint/core",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "**/*.js",
@@ -12,11 +12,11 @@
12
12
  "directory": "packages/core"
13
13
  },
14
14
  "dependencies": {
15
- "@tsslint/types": "1.2.3",
15
+ "@tsslint/types": "1.3.0",
16
16
  "error-stack-parser": "^2.1.4",
17
17
  "esbuild": ">=0.17.0",
18
18
  "minimatch": "^10.0.1",
19
19
  "source-map-support": "^0.5.21"
20
20
  },
21
- "gitHead": "a7b3f988607c37d3e029ec17ff3cec0da5015ea8"
21
+ "gitHead": "3dcc9b0ee6ff8ba8902749edca1892edb5e072d4"
22
22
  }