Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/tsc.ts
13395 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as cp from 'child_process';
7
import * as fs from 'fs';
8
import * as path from 'path';
9
import ts from 'typescript/lib/tsserverlibrary';
10
import { ITestingServicesAccessor } from '../../../src/platform/test/node/services';
11
import { TestingCacheSalts } from '../../base/salts';
12
import { CacheScope } from '../../base/simulationContext';
13
import { REPO_ROOT } from '../../base/stest';
14
import { TS_SERVER_DIAGNOSTICS_PROVIDER_CACHE_SALT } from '../../cacheSalt';
15
import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';
16
import { IFile, ITSDiagnosticRelatedInformation, ITestDiagnostic } from './diagnosticsProvider';
17
import { CachingDiagnosticsProvider, setupTemporaryWorkspace } from './utils';
18
19
/**
20
* Class which finds TS Server diagnostics after compilation of TS files
21
*/
22
export class TSServerDiagnosticsProvider extends CachingDiagnosticsProvider {
23
override readonly id: string;
24
override readonly cacheSalt = TestingCacheSalts.tscCacheSalt;
25
override readonly cacheScope = CacheScope.TSC;
26
27
public readonly ignoreImportErrors: boolean;
28
29
constructor(options: { ignoreImportErrors?: boolean } = {}) {
30
super();
31
this.ignoreImportErrors = options.ignoreImportErrors ?? false;
32
this.id = this.ignoreImportErrors ? 'tsc-ignore-import-errors' : 'tsc';
33
}
34
35
protected override get cacheVersion(): number { return TS_SERVER_DIAGNOSTICS_PROVIDER_CACHE_SALT; }
36
37
protected override async computeDiagnostics(files: IFile[]): Promise<ITestDiagnostic[]> {
38
if (this.ignoreImportErrors) {
39
const identifiers = new Set<string>();
40
for (const file of files) {
41
addIdentifiersToSet(file.fileContents, identifiers);
42
}
43
const filteredIdentifiers = [...withoutKeywords(identifiers)];
44
files.push({
45
fileName: 'modules-mock.d.ts',
46
fileContents: `
47
declare module '*' {
48
${filteredIdentifiers.map(i => `export const ${i}: any; export type ${i} = any;`).join('\n\t')}
49
}
50
`
51
});
52
}
53
54
const workspacePath = await createTempDir();
55
const filesWithPaths = await setupTemporaryWorkspace(workspacePath, files);
56
57
const packagejson = filesWithPaths.find(file => path.basename(file.fileName) === 'package.json');
58
if (packagejson) {
59
try {
60
await doRunNpmInstall(path.dirname(packagejson.filePath));
61
} catch (err) {
62
return files.map(file => ({
63
file: file.fileName,
64
startLine: 0,
65
startCharacter: 0,
66
endLine: 0,
67
endCharacter: 0,
68
code: 'npm-install-failed',
69
message: `npm install failed: ${err.message}`,
70
source: 'ts',
71
relatedInformation: undefined
72
}));
73
}
74
}
75
76
const hasTSConfigFile = filesWithPaths.some(file => path.basename(file.fileName) === 'tsconfig.json');
77
78
if (!hasTSConfigFile) {
79
const tsconfigPath = path.join(workspacePath, 'tsconfig.json');
80
let tsConfig: any;
81
if (this.ignoreImportErrors) {
82
tsConfig = {
83
'compilerOptions': {
84
'target': 'es2021',
85
'strict': true,
86
'module': 'commonjs',
87
'outDir': 'out',
88
'sourceMap': false,
89
'useDefineForClassFields': false,
90
'experimentalDecorators': true,
91
},
92
'exclude': [
93
'node_modules',
94
'outcome',
95
'scenarios'
96
]
97
};
98
} else {
99
tsConfig = {
100
'compilerOptions': {
101
'target': 'es2021',
102
'strict': true,
103
'module': 'commonjs',
104
'outDir': 'out',
105
'sourceMap': true
106
},
107
'exclude': [
108
'node_modules',
109
'outcome',
110
'scenarios'
111
]
112
};
113
}
114
await fs.promises.writeFile(tsconfigPath, JSON.stringify(tsConfig));
115
}
116
117
try {
118
let diagnostics = await this.compileFolder(workspacePath, filesWithPaths);
119
if (this.ignoreImportErrors) {
120
const errorCodeThisMemberCannotHaveAnOverride = 4113; // "This member cannot have an 'override' modifier because it is not declared in the base class 'any'."
121
const errorCodeParameterOptionsImplicitlyHasAnAnyType = 7006; // "Parameter 'options' implicitly has an 'any' type."
122
diagnostics = diagnostics.filter(d => d.code !== errorCodeThisMemberCannotHaveAnOverride && d.code !== errorCodeParameterOptionsImplicitlyHasAnAnyType);
123
}
124
return diagnostics;
125
} finally {
126
cleanTempDirWithRetry(workspacePath);
127
}
128
}
129
130
private compileFolder(workspacePath: string, files: { filePath: string; fileName: string; fileContents: string }[]): Promise<ITestDiagnostic[]> {
131
return new Promise<ITestDiagnostic[]>((resolve, reject) => {
132
const results: ITestDiagnostic[] = [];
133
134
const tsserverPath = path.resolve(path.join(REPO_ROOT, 'node_modules/typescript/lib/tsserver.js'));
135
const tsserver = cp.fork(tsserverPath, {
136
cwd: workspacePath,
137
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
138
});
139
tsserver.stdin?.setDefaultEncoding('utf8');
140
tsserver.stdout?.setEncoding('utf8');
141
142
let seq = 1;
143
const seqToFile = new Map<number, string>();
144
const writeRequest = (data: any) => {
145
data.seq = seq++;
146
const actual = `${JSON.stringify(data)}\r\n`;
147
tsserver.stdin!.write(actual);
148
};
149
150
for (const file of files) {
151
writeRequest({
152
'type': 'request',
153
'command': 'open',
154
'arguments': { 'file': file.filePath }
155
});
156
}
157
for (const file of files) {
158
seqToFile.set(seq, file.fileName);
159
writeRequest({
160
'type': 'request',
161
'command': 'syntacticDiagnosticsSync',
162
'arguments': { 'file': file.filePath }
163
});
164
}
165
for (const file of files) {
166
seqToFile.set(seq, file.fileName);
167
writeRequest({
168
'type': 'request',
169
'command': 'semanticDiagnosticsSync',
170
'arguments': { 'file': file.filePath }
171
});
172
}
173
tsserver.on('error', reject);
174
const handleMessage = (msg: ts.server.protocol.Message) => {
175
if (msg.type !== 'response') {
176
return;
177
}
178
const resp = msg as ts.server.protocol.Response;
179
if (resp.command !== 'semanticDiagnosticsSync' && resp.command !== 'syntacticDiagnosticsSync') {
180
return;
181
}
182
const kind = resp.command === 'semanticDiagnosticsSync' ? 'semantic' : 'syntactic';
183
const diagResp = resp as ts.server.protocol.SemanticDiagnosticsSyncResponse | ts.server.protocol.SyntacticDiagnosticsSyncResponse;
184
for (const diag of diagResp.body ?? []) {
185
if (typeof diag.start === 'number') {
186
throw new Error(`TODO: Can't handle DiagnosticWithLinePosition right now`);
187
}
188
const regularDiag = diag as ts.server.protocol.Diagnostic;
189
const _relatedInfo: (ITSDiagnosticRelatedInformation | null)[] = (regularDiag.relatedInformation ?? []).map((ri) => {
190
if (!ri.span) {
191
return null;
192
}
193
return {
194
location: {
195
file: ri.span.file.substring(workspacePath.length + 1),
196
startLine: ri.span?.start.line - 1,
197
startCharacter: ri.span?.start.offset - 1,
198
endLine: ri.span?.end.line - 1,
199
endCharacter: ri.span?.end.offset - 1,
200
},
201
message: ri.message,
202
code: ri.code
203
};
204
});
205
const relatedInformation = _relatedInfo.filter((x): x is ITSDiagnosticRelatedInformation => !!x);
206
results.push({
207
file: seqToFile.get(diagResp.request_seq)!,
208
startLine: regularDiag.start.line - 1,
209
startCharacter: regularDiag.start.offset - 1,
210
endLine: regularDiag.end.line - 1,
211
endCharacter: regularDiag.end.offset - 1,
212
message: regularDiag.text,
213
code: regularDiag.code,
214
relatedInformation,
215
source: 'ts',
216
kind,
217
});
218
}
219
220
if (diagResp.request_seq === seq - 1) {
221
writeRequest({
222
'type': 'request',
223
'command': 'exit',
224
});
225
tsserver.on('exit', () => {
226
resolve(results);
227
});
228
tsserver.kill();
229
}
230
};
231
232
let stdout = '';
233
const processStdoutData = () => {
234
do {
235
const eolIndex = stdout.indexOf('\r\n') ?? stdout.indexOf('\n');
236
if (eolIndex === -1) {
237
break;
238
}
239
const firstLine = stdout.substring(0, eolIndex);
240
let body;
241
if (firstLine.includes('Content-Length')) {
242
const contentLength = parseInt(firstLine.substring('Content-Length: '.length), 10);
243
body = stdout.substring(eolIndex + 4, eolIndex + 4 + contentLength);
244
if (body.length < contentLength) {
245
// entire body did not arrive yet
246
break;
247
}
248
stdout = stdout.substring(eolIndex + 4 + contentLength);
249
} else {
250
// Might come after the body
251
body = firstLine;
252
// Hold on to the rest of the stdout for the next iteration
253
stdout = stdout.substring(eolIndex + 2);
254
}
255
256
try {
257
handleMessage(JSON.parse(body));
258
} catch (ex) {
259
console.error(ex);
260
}
261
} while (true);
262
};
263
264
tsserver.stdout!.on('data', (chunk) => {
265
stdout += chunk;
266
processStdoutData();
267
});
268
});
269
}
270
}
271
272
function addIdentifiersToSet(content: string, result: Set<string>): void {
273
const regex = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
274
let match: RegExpExecArray | null;
275
while ((match = regex.exec(content)) !== null) {
276
result.add(match[0]);
277
}
278
}
279
280
function withoutKeywords(identifiers: Set<string>): Set<string> {
281
const keywords = ['class', 'interface', 'function', 'const', 'let', 'var', 'import', 'export', 'from', 'default', 'extends', 'implements', 'new', 'return', 'if', 'else', 'for',
282
'while', 'do', 'switch', 'case', 'break', 'continue', 'throw', 'try', 'catch', 'finally', 'finally', 'await', 'async', 'await', 'void', 'any', 'number',
283
'string', 'boolean', 'object', 'null', 'undefined', 'true', 'false', 'this', 'super', 'typeof', 'instanceof', 'in', 'as', 'is', 'delete', 'typeof',
284
'instanceof', 'in', 'as', 'is', 'delete', 'void', 'never', 'unknown', 'declare', 'namespace', 'module', 'type', 'enum', 'readonly', 'abstract', 'private',
285
'protected', 'public', 'static', 'readonly', 'abstract', 'private', 'protected', 'public', 'static', 'get', 'set', 'constructor', 'require', 'module', 'exports',
286
'global', 'window', 'document', 'console', 'process', 'require', 'module', 'exports', 'global', 'window', 'document', 'console', 'process', 'with'];
287
const keywordsSet = new Set(keywords);
288
const filteredIdentifiers = new Set<string>();
289
for (const identifier of identifiers) {
290
if (!keywordsSet.has(identifier)) {
291
filteredIdentifiers.add(identifier);
292
}
293
}
294
return filteredIdentifiers;
295
}
296
297
/**
298
* Runs `npm install` and proceeds to compile the files in the passed in folder. This is cached and is safe to use in tests.
299
*/
300
export async function compileTSWorkspace(accessor: ITestingServicesAccessor, folderPath: string): Promise<ITestDiagnostic[]> {
301
const files = await readTSFiles(folderPath);
302
return await new TSServerDiagnosticsProvider().getDiagnostics(accessor, files);
303
}
304
305
export function doRunNpmInstall(projectRoot: string): Promise<void> {
306
return new Promise((resolve, reject) => {
307
cp.exec('npm install', { cwd: projectRoot }, (error, stdout, stderr) => {
308
if (error) {
309
return reject(error);
310
}
311
return resolve();
312
});
313
});
314
}
315
316
async function readTSFiles(folderPath: string): Promise<IFile[]> {
317
const allFiles: string[] = [];
318
await rreaddir(folderPath, allFiles);
319
return await Promise.all(
320
allFiles.filter(
321
file => ['.ts', '.tsx', '.json'].includes(path.extname(file))
322
).map(async (filePath) => {
323
const relativeFilePath = path.relative(folderPath, filePath);
324
const fileContents = await fs.promises.readFile(filePath, 'utf8');
325
return {
326
fileName: relativeFilePath,
327
fileContents
328
};
329
})
330
);
331
}
332
333
async function rreaddir(folderPath: string, result: string[]): Promise<void> {
334
const entries = await fs.promises.readdir(folderPath, { withFileTypes: true });
335
for (const entry of entries) {
336
const fullPath = path.join(folderPath, entry.name);
337
if (entry.isDirectory()) {
338
await rreaddir(fullPath, result);
339
} else {
340
result.push(fullPath);
341
}
342
}
343
}
344
345