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