function analyze()

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
	};
}