Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/nls-analysis.ts
13379 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 ts from 'typescript';
7
8
// ============================================================================
9
// Types
10
// ============================================================================
11
12
export interface ISpan {
13
start: ts.LineAndCharacter;
14
end: ts.LineAndCharacter;
15
}
16
17
export interface ILocalizeCall {
18
keySpan: ISpan;
19
key: string;
20
valueSpan: ISpan;
21
value: string;
22
}
23
24
// ============================================================================
25
// AST Collection
26
// ============================================================================
27
28
export const CollectStepResult = Object.freeze({
29
Yes: 'Yes',
30
YesAndRecurse: 'YesAndRecurse',
31
No: 'No',
32
NoAndRecurse: 'NoAndRecurse'
33
});
34
35
export type CollectStepResult = typeof CollectStepResult[keyof typeof CollectStepResult];
36
37
export function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] {
38
const result: ts.Node[] = [];
39
40
function loop(node: ts.Node) {
41
const stepResult = fn(node);
42
43
if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) {
44
result.push(node);
45
}
46
47
if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) {
48
ts.forEachChild(node, loop);
49
}
50
}
51
52
loop(node);
53
return result;
54
}
55
56
export function isImportNode(node: ts.Node): boolean {
57
return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration;
58
}
59
60
export function isCallExpressionWithinTextSpanCollectStep(textSpan: ts.TextSpan, node: ts.Node): CollectStepResult {
61
if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) {
62
return CollectStepResult.No;
63
}
64
65
return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse;
66
}
67
68
// ============================================================================
69
// Language Service Host
70
// ============================================================================
71
72
export class SingleFileServiceHost implements ts.LanguageServiceHost {
73
private file: ts.IScriptSnapshot;
74
private lib: ts.IScriptSnapshot;
75
private options: ts.CompilerOptions;
76
private filename: string;
77
78
constructor(options: ts.CompilerOptions, filename: string, contents: string) {
79
this.options = options;
80
this.filename = filename;
81
this.file = ts.ScriptSnapshot.fromString(contents);
82
this.lib = ts.ScriptSnapshot.fromString('');
83
}
84
85
getCompilationSettings = () => this.options;
86
getScriptFileNames = () => [this.filename];
87
getScriptVersion = () => '1';
88
getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib;
89
getCurrentDirectory = () => '';
90
getDefaultLibFileName = () => 'lib.d.ts';
91
92
readFile(path: string): string | undefined {
93
if (path === this.filename) {
94
return this.file.getText(0, this.file.getLength());
95
}
96
return undefined;
97
}
98
99
fileExists(path: string): boolean {
100
return path === this.filename;
101
}
102
}
103
104
// ============================================================================
105
// Analysis
106
// ============================================================================
107
108
/**
109
* Analyzes TypeScript source code to find localize() or localize2() calls.
110
*/
111
export function analyzeLocalizeCalls(
112
contents: string,
113
functionName: 'localize' | 'localize2'
114
): ILocalizeCall[] {
115
const filename = 'file.ts';
116
const options: ts.CompilerOptions = { noResolve: true };
117
const serviceHost = new SingleFileServiceHost(options, filename, contents);
118
const service = ts.createLanguageService(serviceHost);
119
const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true);
120
121
// Find all imports
122
const imports = collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse);
123
124
// import nls = require('vs/nls');
125
const importEqualsDeclarations = imports
126
.filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration)
127
.map(n => n as ts.ImportEqualsDeclaration)
128
.filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference)
129
.filter(d => {
130
const text = (d.moduleReference as ts.ExternalModuleReference).expression.getText();
131
return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`);
132
});
133
134
// import ... from 'vs/nls';
135
const importDeclarations = imports
136
.filter(n => n.kind === ts.SyntaxKind.ImportDeclaration)
137
.map(n => n as ts.ImportDeclaration)
138
.filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
139
.filter(d => {
140
const text = d.moduleSpecifier.getText();
141
return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`);
142
})
143
.filter(d => !!d.importClause && !!d.importClause.namedBindings);
144
145
// `nls.localize(...)` calls via namespace import
146
const nlsLocalizeCallExpressions: ts.CallExpression[] = [];
147
148
const namespaceImports = importDeclarations
149
.filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport)
150
.map(d => (d.importClause!.namedBindings as ts.NamespaceImport).name);
151
152
const importEqualsNames = importEqualsDeclarations.map(d => d.name);
153
154
for (const name of [...namespaceImports, ...importEqualsNames]) {
155
const refs = service.getReferencesAtPosition(filename, name.pos + 1) ?? [];
156
for (const ref of refs) {
157
if (ref.isWriteAccess) {
158
continue;
159
}
160
const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n));
161
const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined;
162
if (lastCall &&
163
lastCall.expression.kind === ts.SyntaxKind.PropertyAccessExpression &&
164
(lastCall.expression as ts.PropertyAccessExpression).name.getText() === functionName) {
165
nlsLocalizeCallExpressions.push(lastCall);
166
}
167
}
168
}
169
170
// `localize` named imports
171
const namedImports = importDeclarations
172
.filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports)
173
.flatMap(d => Array.from((d.importClause!.namedBindings! as ts.NamedImports).elements));
174
175
const localizeCallExpressions: ts.CallExpression[] = [];
176
177
// Direct named import: import { localize } from 'vs/nls'
178
for (const namedImport of namedImports) {
179
const isTarget = namedImport.name.getText() === functionName ||
180
(namedImport.propertyName && namedImport.propertyName.getText() === functionName);
181
182
if (!isTarget) {
183
continue;
184
}
185
186
const searchName = namedImport.propertyName ? namedImport.name : namedImport.name;
187
const refs = service.getReferencesAtPosition(filename, searchName.pos + 1) ?? [];
188
189
for (const ref of refs) {
190
if (ref.isWriteAccess) {
191
continue;
192
}
193
const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n));
194
const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined;
195
if (lastCall) {
196
localizeCallExpressions.push(lastCall);
197
}
198
}
199
}
200
201
// Combine and deduplicate
202
const allCalls = [...nlsLocalizeCallExpressions, ...localizeCallExpressions];
203
const seen = new Set<number>();
204
const uniqueCalls = allCalls.filter(call => {
205
const start = call.getStart();
206
if (seen.has(start)) {
207
return false;
208
}
209
seen.add(start);
210
return true;
211
});
212
213
// Convert to ILocalizeCall
214
return uniqueCalls
215
.filter(e => e.arguments.length > 1)
216
.sort((a, b) => a.arguments[0].getStart() - b.arguments[0].getStart())
217
.map(e => {
218
const args = e.arguments;
219
return {
220
keySpan: {
221
start: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getStart()),
222
end: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getEnd())
223
},
224
key: args[0].getText(),
225
valueSpan: {
226
start: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getStart()),
227
end: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getEnd())
228
},
229
value: args[1].getText()
230
};
231
});
232
}
233
234
// ============================================================================
235
// Text Model for patching
236
// ============================================================================
237
238
export class TextModel {
239
private lines: string[];
240
private lineEndings: string[];
241
242
constructor(contents: string) {
243
const regex = /\r\n|\r|\n/g;
244
let index = 0;
245
let match: RegExpExecArray | null;
246
247
this.lines = [];
248
this.lineEndings = [];
249
250
while (match = regex.exec(contents)) {
251
this.lines.push(contents.substring(index, match.index));
252
this.lineEndings.push(match[0]);
253
index = regex.lastIndex;
254
}
255
256
if (contents.length > 0) {
257
this.lines.push(contents.substring(index, contents.length));
258
this.lineEndings.push('');
259
}
260
}
261
262
get(index: number): string {
263
return this.lines[index];
264
}
265
266
set(index: number, line: string): void {
267
this.lines[index] = line;
268
}
269
270
get lineCount(): number {
271
return this.lines.length;
272
}
273
274
/**
275
* Applies patch(es) to the model.
276
* Multiple patches must be ordered.
277
* Does not support patches spanning multiple lines.
278
*/
279
apply(span: ISpan, content: string): void {
280
const startLineNumber = span.start.line;
281
const endLineNumber = span.end.line;
282
283
const startLine = this.lines[startLineNumber] || '';
284
const endLine = this.lines[endLineNumber] || '';
285
286
this.lines[startLineNumber] = [
287
startLine.substring(0, span.start.character),
288
content,
289
endLine.substring(span.end.character)
290
].join('');
291
292
for (let i = startLineNumber + 1; i <= endLineNumber; i++) {
293
this.lines[i] = '';
294
}
295
}
296
297
toString(): string {
298
let result = '';
299
for (let i = 0; i < this.lines.length; i++) {
300
result += this.lines[i] + this.lineEndings[i];
301
}
302
return result;
303
}
304
}
305
306
// ============================================================================
307
// Utilities
308
// ============================================================================
309
310
/**
311
* Parses a localize key or value expression.
312
* sourceExpression can be "foo", 'foo', `foo` or { key: 'foo', comment: [...] }
313
*/
314
export function parseLocalizeKeyOrValue(sourceExpression: string): string | { key: string; comment?: string[] } {
315
// eslint-disable-next-line no-eval
316
return eval(`(${sourceExpression})`);
317
}
318
319