Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/propertyInitOrderChecker.ts
5251 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
7
import * as ts from 'typescript';
8
import * as path from 'path';
9
import * as fs from 'fs';
10
11
const TS_CONFIG_PATH = path.join(import.meta.dirname, '../../', 'src', 'tsconfig.json');
12
13
//
14
// #############################################################################################
15
//
16
// A custom typescript checker that ensure constructor properties are NOT used to initialize
17
// defined properties. This is needed for the times when `useDefineForClassFields` is gone.
18
//
19
// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726,
20
// https://github.com/microsoft/vscode/pull/241544
21
//
22
// #############################################################################################
23
//
24
25
const EntryKind = Object.freeze({
26
Span: 'Span',
27
Node: 'Node',
28
StringLiteral: 'StringLiteral',
29
SearchedLocalFoundProperty: 'SearchedLocalFoundProperty',
30
SearchedPropertyFoundLocal: 'SearchedPropertyFoundLocal'
31
});
32
33
type EntryKind = typeof EntryKind[keyof typeof EntryKind];
34
35
const cancellationToken: ts.CancellationToken = {
36
isCancellationRequested: () => false,
37
throwIfCancellationRequested: () => { },
38
};
39
40
const seenFiles = new Set<ts.SourceFile>();
41
let errorCount = 0;
42
43
44
45
function createProgram(tsconfigPath: string): ts.Program {
46
const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
47
48
const configHostParser: ts.ParseConfigHost = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' };
49
const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true });
50
51
const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true);
52
53
return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost);
54
}
55
56
const program = createProgram(TS_CONFIG_PATH);
57
58
program.getTypeChecker();
59
60
for (const file of program.getSourceFiles()) {
61
if (!file || file.isDeclarationFile) {
62
continue;
63
}
64
visit(file);
65
}
66
67
if (seenFiles.size) {
68
console.log();
69
console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`);
70
process.exit(errorCount);
71
}
72
73
function visit(node: ts.Node) {
74
if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) {
75
checkParameterPropertyDeclaration(node);
76
}
77
78
ts.forEachChild(node, visit);
79
}
80
81
function checkParameterPropertyDeclaration(param: ts.ParameterPropertyDeclaration) {
82
const uses = [...collectReferences(param.name, [])];
83
if (!uses.length) {
84
return;
85
}
86
87
const sourceFile = param.getSourceFile();
88
if (!seenFiles.has(sourceFile)) {
89
if (seenFiles.size) {
90
console.log(``);
91
}
92
console.log(`${formatFileName(param)}:`);
93
seenFiles.add(sourceFile);
94
} else {
95
console.log(``);
96
}
97
console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`);
98
for (const { stack, container } of uses) {
99
const use = stack[stack.length - 1];
100
console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`);
101
errorCount++;
102
}
103
}
104
105
interface InvalidUse {
106
stack: ts.Node[];
107
container: ReferenceContainer;
108
}
109
110
function* collectReferences(node: ts.Node, stack: ts.Node[], requiresInvocationDepth: number = 0, seen = new Set<ReferenceContainer>()): Generator<InvalidUse> {
111
for (const use of findAllReferencesInClass(node)) {
112
const container = findContainer(use);
113
if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) {
114
continue;
115
}
116
seen.add(container);
117
118
const nextStack = [...stack, use];
119
120
let nextRequiresInvocationDepth = requiresInvocationDepth;
121
if (isInvocation(use) && nextRequiresInvocationDepth > 0) {
122
nextRequiresInvocationDepth--;
123
}
124
125
if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) {
126
yield { stack: nextStack, container };
127
}
128
else if (requiresInvocation(container)) {
129
nextRequiresInvocationDepth++;
130
}
131
132
yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen);
133
}
134
}
135
136
function requiresInvocation(definition: ReferenceContainer): boolean {
137
return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition);
138
}
139
140
function isInvocation(use: ts.Node): boolean {
141
let location = use;
142
if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) {
143
location = location.parent;
144
}
145
else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) {
146
location = location.parent;
147
}
148
return ts.isCallExpression(location.parent) && location.parent.expression === location
149
|| ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location;
150
}
151
152
function formatFileName(node: ts.Node): string {
153
const sourceFile = node.getSourceFile();
154
return path.resolve(sourceFile.fileName);
155
}
156
157
function formatLocation(node: ts.Node): string {
158
const sourceFile = node.getSourceFile();
159
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
160
return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`;
161
}
162
163
function formatStack(stack: ts.Node[]): string {
164
return stack.slice().reverse().map((use) => formatUse(use)).join(' -> ');
165
}
166
167
function formatMember(container: ReferenceContainer): string {
168
const name = container.name?.getText();
169
if (name) {
170
const className = findClass(container)?.name?.getText();
171
if (className) {
172
return `${className}.${name}`;
173
}
174
return name;
175
}
176
return '<unknown>';
177
}
178
179
function formatUse(use: ts.Node): string {
180
let text = use.getText();
181
if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) {
182
if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) {
183
text = `this.${text}`;
184
}
185
use = use.parent;
186
}
187
else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) {
188
if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) {
189
text = `this['${text}']`;
190
}
191
use = use.parent;
192
}
193
if (ts.isCallExpression(use.parent)) {
194
text = `${text}(...)`;
195
}
196
return text;
197
}
198
199
type ReferenceContainer =
200
| ts.PropertyDeclaration
201
| ts.MethodDeclaration
202
| ts.GetAccessorDeclaration
203
| ts.SetAccessorDeclaration
204
| ts.ConstructorDeclaration
205
| ts.ClassStaticBlockDeclaration
206
| ts.ArrowFunction
207
| ts.FunctionExpression
208
| ts.FunctionDeclaration
209
| ts.ParameterDeclaration;
210
211
function findContainer(node: ts.Node): ReferenceContainer | undefined {
212
return ts.findAncestor(node, ancestor => {
213
switch (ancestor.kind) {
214
case ts.SyntaxKind.PropertyDeclaration:
215
case ts.SyntaxKind.MethodDeclaration:
216
case ts.SyntaxKind.GetAccessor:
217
case ts.SyntaxKind.SetAccessor:
218
case ts.SyntaxKind.Constructor:
219
case ts.SyntaxKind.ClassStaticBlockDeclaration:
220
case ts.SyntaxKind.ArrowFunction:
221
case ts.SyntaxKind.FunctionExpression:
222
case ts.SyntaxKind.FunctionDeclaration:
223
case ts.SyntaxKind.Parameter:
224
return true;
225
}
226
return false;
227
}) as ReferenceContainer | undefined;
228
}
229
230
function findClass(node: ts.Node): ts.ClassLikeDeclaration | undefined {
231
return ts.findAncestor(node, ts.isClassLike);
232
}
233
234
function* findAllReferencesInClass(node: ts.Node): Generator<ts.Node> {
235
const classDecl = findClass(node);
236
if (!classDecl) {
237
return [];
238
}
239
for (const ref of findAllReferences(node)) {
240
for (const entry of ref.references) {
241
if (entry.kind !== EntryKind.Node || entry.node === node) {
242
continue;
243
}
244
if (findClass(entry.node) === classDecl) {
245
yield entry.node;
246
}
247
}
248
}
249
}
250
251
// NOTE: The following uses TypeScript internals and are subject to change from version to version.
252
253
interface TypeScriptInternals {
254
getTouchingPropertyName(sourceFile: ts.SourceFile, position: number): ts.Node;
255
FindAllReferences: {
256
FindReferencesUse: {
257
References: number;
258
};
259
Core: {
260
getReferencedSymbolsForNode(
261
position: number,
262
node: ts.Node,
263
program: ts.Program,
264
sourceFiles: readonly ts.SourceFile[],
265
cancellationToken: ts.CancellationToken,
266
options: { use: number }
267
): readonly SymbolAndEntries[] | undefined;
268
};
269
};
270
}
271
272
function findAllReferences(node: ts.Node): readonly SymbolAndEntries[] {
273
const sourceFile = node.getSourceFile();
274
const position = node.getStart();
275
const tsInternal = ts as unknown as TypeScriptInternals;
276
const name: ts.Node = tsInternal.getTouchingPropertyName(sourceFile, position);
277
const options = { use: tsInternal.FindAllReferences.FindReferencesUse.References };
278
return tsInternal.FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? [];
279
}
280
281
interface SymbolAndEntries {
282
readonly definition: Definition | undefined;
283
readonly references: readonly Entry[];
284
}
285
286
const DefinitionKind = Object.freeze({
287
Symbol: 0,
288
Label: 1,
289
Keyword: 2,
290
This: 3,
291
String: 4,
292
TripleSlashReference: 5,
293
});
294
type DefinitionKind = typeof DefinitionKind[keyof typeof DefinitionKind];
295
296
type Definition =
297
| { readonly type: DefinitionKind; readonly symbol: ts.Symbol }
298
| { readonly type: DefinitionKind; readonly node: ts.Identifier }
299
| { readonly type: DefinitionKind; readonly node: ts.Node }
300
| { readonly type: DefinitionKind; readonly node: ts.Node }
301
| { readonly type: DefinitionKind; readonly node: ts.StringLiteralLike }
302
| { readonly type: DefinitionKind; readonly reference: ts.FileReference; readonly file: ts.SourceFile };
303
304
type NodeEntryKind = typeof EntryKind.Node | typeof EntryKind.StringLiteral | typeof EntryKind.SearchedLocalFoundProperty | typeof EntryKind.SearchedPropertyFoundLocal;
305
type Entry = NodeEntry | SpanEntry;
306
interface ContextWithStartAndEndNode {
307
start: ts.Node;
308
end: ts.Node;
309
}
310
type ContextNode = ts.Node | ContextWithStartAndEndNode;
311
interface NodeEntry {
312
readonly kind: NodeEntryKind;
313
readonly node: ts.Node;
314
readonly context?: ContextNode;
315
}
316
interface SpanEntry {
317
readonly kind: typeof EntryKind.Span;
318
readonly fileName: string;
319
readonly textSpan: ts.TextSpan;
320
}
321
322