in src/lib.ts [350:641]
function analyze(contents: string, relativeFilename: string | undefined, options: ts.CompilerOptions = {}): AnalysisResult {
const vscodeRegExp = /^\s*(["'])vscode-nls\1\s*$/;
enum CollectStepResult {
Yes,
YesAndRecurse,
No,
NoAndRecurse
}
function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] {
const result: ts.Node[] = [];
function loop(node: ts.Node) {
const stepResult = fn(node);
if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) {
result.push(node);
}
if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) {
ts.forEachChild(node, loop);
}
}
loop(node);
return result;
}
function isImportNode(node: ts.Node): boolean {
if (ts.isImportDeclaration(node)) {
return ts.isStringLiteralLike(node.moduleSpecifier) && vscodeRegExp.test(node.moduleSpecifier.getText());
}
if (ts.isImportEqualsDeclaration(node)) {
return ts.isExternalModuleReference(node.moduleReference)
&& ts.isStringLiteralLike(node.moduleReference.expression)
&& vscodeRegExp.test(node.moduleReference.expression.getText());
}
return false;
}
function isRequireImport(node: ts.Node): boolean {
if (!ts.isCallExpression(node)) {
return false;
}
if (node.expression.getText() !== 'require' || !node.arguments || node.arguments.length !== 1) {
return false;
}
const argument = node.arguments[0];
return ts.isStringLiteralLike(argument) && vscodeRegExp.test(argument.getText());
}
function findClosestNode(node: ts.Node, textSpan: ts.TextSpan): ts.Node | undefined {
let textSpanEnd = textSpan.start + textSpan.length;
function loop(node: ts.Node): ts.Node | undefined {
const length = node.end - node.pos;
if (node.pos === textSpan.start && length === textSpan.length) {
return node;
}
if (node.pos <= textSpan.start && textSpanEnd <= node.end) {
const candidate = ts.forEachChild(node, loop);
return candidate || node;
}
return undefined;
}
return loop(node);
}
const unescapeMap: Map<string> = {
'\'': '\'',
'"': '"',
'\\': '\\',
'n': '\n',
'r': '\r',
't': '\t',
'b': '\b',
'f': '\f'
};
function unescapeString(str: string): string {
const result: string[] = [];
for (let i = 0; i < str.length; i++) {
const ch = str.charAt(i);
if (ch === '\\') {
if (i + 1 < str.length) {
let replace = unescapeMap[str.charAt(i + 1)];
if (replace !== undefined) {
result.push(replace);
i++;
continue;
}
}
}
result.push(ch);
}
return result.join('');
}
options = clone(options, false);
options.noResolve = true;
options.allowJs = true;
const filename = 'file.js';
const serviceHost = new SingleFileServiceHost(options, filename, contents);
const service = ts.createLanguageService(serviceHost);
const sourceFile = service.getProgram()!.getSourceFile(filename)!;
const patches: Patch[] = [];
const errors: string[] = [];
const bundle: JavaScriptMessageBundle = { messages: [], keys: [] };
// all imports
const imports = collect(sourceFile, n => isRequireImport(n) || isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse);
const nlsReferences = imports.reduce<ts.Node[]>((memo, node) => {
let references: ts.ReferenceEntry[] | undefined;
if (ts.isCallExpression(node)) {
let parent = node.parent;
if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression) && parent.expression.text === '__importStar') {
parent = node.parent.parent;
}
if (ts.isVariableDeclaration(parent)) {
references = service.getReferencesAtPosition(filename, parent.name.pos + 1);
}
} else if (ts.isImportDeclaration(node) && node.importClause && node.importClause.namedBindings) {
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
references = service.getReferencesAtPosition(filename, node.importClause.namedBindings.pos);
}
} else if (ts.isImportEqualsDeclaration(node)) {
references = service.getReferencesAtPosition(filename, node.name.pos);
}
if (references) {
references.forEach(reference => {
if (!reference.isWriteAccess) {
const node = findClosestNode(sourceFile, reference.textSpan);
if (node) {
memo.push(node);
}
}
});
}
return memo;
}, []);
const loadCalls = nlsReferences.reduce<ts.CallExpression[]>((memo, node) => {
// We are looking for nls.loadMessageBundle || nls.config. In the AST
// this is Indetifier -> PropertyAccess -> CallExpression.
if (!ts.isIdentifier(node) || !ts.isPropertyAccessExpression(node.parent) || !ts.isCallExpression(node.parent.parent)) {
return memo;
}
const callExpression = node.parent.parent;
const expression = callExpression.expression;
if (ts.isPropertyAccessExpression(expression)) {
if (expression.name.text === 'loadMessageBundle') {
// We have a load call like nls.loadMessageBundle();
memo.push(callExpression);
} else if (expression.name.text === 'config') {
// We have a load call like nls.config({...})();
let parent = callExpression.parent;
if (ts.isCallExpression(parent) && parent.expression === callExpression) {
memo.push(parent);
}
}
}
return memo;
}, []);
const localizeCalls = loadCalls.reduce<ts.CallExpression[]>((memo, loadCall) => {
const parent = loadCall.parent;
if (ts.isCallExpression(parent)) {
// We have something like nls.config({...})()('key', 'message');
memo.push(parent);
} else if (ts.isVariableDeclaration(parent)) {
// We have something like var localize = nls.config({...})();
const references = service.getReferencesAtPosition(filename, parent.name.pos + 1);
if (references) {
references.forEach(reference => {
if (!reference.isWriteAccess) {
const node = findClosestNode(sourceFile, reference.textSpan);
if (node) {
if (ts.isIdentifier(node)) {
let parent = node.parent;
if (ts.isCallExpression(parent) && parent.arguments.length >= 2) {
memo.push(parent);
} else {
let position = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
errors.push(`(${position.line + 1},${position.character + 1}): localize function (bound to ${node.text}) used in an unusual way.`);
}
}
}
}
});
}
}
return memo;
}, []);
loadCalls.reduce((memo, loadCall) => {
if (loadCall.arguments.length === 0) {
const args = loadCall.arguments;
patches.push({
span: { start: ts.getLineAndCharacterOfPosition(sourceFile, args.pos), end: ts.getLineAndCharacterOfPosition(sourceFile, args.end) },
content: relativeFilename ? `require('path').join(__dirname, '${relativeFilename.replace(/\\/g, '\\\\')}')` : '__filename',
});
}
return memo;
}, patches);
let messageIndex = 0;
localizeCalls.reduce((memo, localizeCall) => {
const firstArg = localizeCall.arguments[0];
const secondArg = localizeCall.arguments[1];
let key: string | null = null;
let message: string | null = null;
let comment: string[] = [];
let text: string | null = null;
if (ts.isStringLiteralLike(firstArg)) {
text = firstArg.getText();
key = text.substr(1, text.length - 2);
} else if (ts.isObjectLiteralExpression(firstArg)) {
for (let i = 0; i < firstArg.properties.length; i++) {
const property = firstArg.properties[i];
if (ts.isPropertyAssignment(property)) {
const name = property.name.getText();
if (name === 'key') {
const initializer = property.initializer;
if (ts.isStringLiteralLike(initializer)) {
text = initializer.getText();
key = text.substr(1, text.length - 2);
}
} else if (name === 'comment') {
const initializer = property.initializer;
if (ts.isArrayLiteralExpression(initializer)) {
initializer.elements.forEach(element => {
if (ts.isStringLiteralLike(element)) {
text = element.getText();
comment.push(text.substr(1, text.length - 2));
}
});
}
}
}
}
}
if (!key) {
const position = ts.getLineAndCharacterOfPosition(sourceFile, firstArg.pos);
errors.push(`(${position.line + 1},${position.character + 1}): first argument of a localize call must either be a string literal or an object literal of type LocalizeInfo.`);
return memo;
}
if (ts.isStringLiteralLike(secondArg)) {
const text = secondArg.getText();
message = text.substr(1, text.length - 2);
}
if (!message) {
const position = ts.getLineAndCharacterOfPosition(sourceFile, secondArg.pos);
errors.push(`(${position.line + 1},${position.character + 1}): second argument of a localize call must be a string literal.`);
return memo;
}
message = unescapeString(message);
memo.patches.push({
span: { start: ts.getLineAndCharacterOfPosition(sourceFile, firstArg.pos + firstArg.getLeadingTriviaWidth()), end: ts.getLineAndCharacterOfPosition(sourceFile, firstArg.end) },
content: messageIndex.toString()
});
memo.patches.push({
span: { start: ts.getLineAndCharacterOfPosition(sourceFile, secondArg.pos + secondArg.getLeadingTriviaWidth()), end: ts.getLineAndCharacterOfPosition(sourceFile, secondArg.end) },
content: 'null'
});
bundle.messages.push(message);
if (comment.length > 0) {
bundle.keys.push({
key: key,
comment: comment
});
} else {
bundle.keys.push(key);
}
messageIndex++;
return memo;
}, { patches });
return {
patches,
errors,
bundle
};
}