@tsslint/core 1.2.4 → 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());
@@ -205,7 +286,7 @@ function createLinter(ctx, config, withStack) {
205
286
  }
206
287
  }
207
288
  fixes.set(error, []);
208
- diagnostics.push(error);
289
+ (cacheable ? cacheableDiagnostics : uncacheableDiagnostics).push(error);
209
290
  currentIssues++;
210
291
  return {
211
292
  withDeprecated() {
@@ -279,8 +360,8 @@ function createLinter(ctx, config, withStack) {
279
360
  }
280
361
  return false;
281
362
  },
282
- getCodeFixes(fileName, start, end, diagnostics) {
283
- const configs = getFileConfigs(fileName);
363
+ getCodeFixes(fileName, start, end, diagnostics, cache) {
364
+ const configs = getFileConfigs(fileName, cache);
284
365
  const fixesMap = getFileFixes(fileName);
285
366
  const result = [];
286
367
  for (const [diagnostic, actions] of fixesMap) {
@@ -306,7 +387,7 @@ function createLinter(ctx, config, withStack) {
306
387
  for (const { plugins } of configs) {
307
388
  for (const { resolveCodeFixes } of plugins) {
308
389
  if (resolveCodeFixes) {
309
- codeFixes = resolveCodeFixes(fileName, diagnostic, codeFixes);
390
+ codeFixes = resolveCodeFixes(getSourceFile(fileName), diagnostic, codeFixes);
310
391
  }
311
392
  }
312
393
  }
@@ -346,11 +427,28 @@ function createLinter(ctx, config, withStack) {
346
427
  }
347
428
  },
348
429
  };
349
- 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) {
350
448
  let result = fileRules.get(fileName);
351
449
  if (!result) {
352
450
  result = {};
353
- const configs = getFileConfigs(fileName);
451
+ const configs = getFileConfigs(fileName, cache);
354
452
  for (const { rules } of configs) {
355
453
  result = {
356
454
  ...result,
@@ -368,19 +466,36 @@ function createLinter(ctx, config, withStack) {
368
466
  }
369
467
  return result;
370
468
  }
371
- function getFileConfigs(fileName) {
469
+ function getFileConfigs(fileName, cache) {
372
470
  let result = fileConfigs.get(fileName);
373
471
  if (!result) {
374
- result = configs.filter(({ includes, excludes }) => {
375
- if (excludes.some(pattern => minimatch.minimatch(fileName, pattern))) {
472
+ result = configs.filter(({ include, exclude }) => {
473
+ if (exclude.some(_minimatch)) {
376
474
  return false;
377
475
  }
378
- if (includes.length && !includes.some(pattern => minimatch.minimatch(fileName, pattern))) {
476
+ if (include.length && !include.some(_minimatch)) {
379
477
  return false;
380
478
  }
381
479
  return true;
382
480
  });
383
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
+ }
384
499
  }
385
500
  return result;
386
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.4",
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.4",
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": "818bc257e90e431ca8988477862238c70a4757ff"
21
+ "gitHead": "3dcc9b0ee6ff8ba8902749edca1892edb5e072d4"
22
22
  }