diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fa94426d27..60164e3ea97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,5 @@ "extensions/**/out/**": true }, "tslint.enable": true, - "tslint.rulesDirectory": "node_modules/tslint-microsoft-contrib" + "tslint.rulesDirectory": "build/tslintRules" } \ No newline at end of file diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index f5dd30db8cf..615e2df3798 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -104,7 +104,7 @@ var lintReporter = function (output, file, options) { gulp.task('tslint', function () { return gulp.src(all, { base: '.' }) .pipe(filter(tslintFilter)) - .pipe(tslint({ rulesDirectory: 'node_modules/tslint-microsoft-contrib' })) + .pipe(tslint({ rulesDirectory: 'build/tslintRules' })) .pipe(tslint.report(lintReporter, { summarizeFailureOutput: false, emitError: false diff --git a/build/tslintRules/noUnexternalizedStringsRule.js b/build/tslintRules/noUnexternalizedStringsRule.js new file mode 100644 index 00000000000..cb04dc0885a --- /dev/null +++ b/build/tslintRules/noUnexternalizedStringsRule.js @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +"use strict"; +var __extends = (this && this.__extends) || function(d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var ts = require('typescript'); +var Lint = require('tslint/lib/lint'); +/** + * Implementation of the no-unexternalized-strings rule. + */ +var Rule = (function(_super) { + __extends(Rule, _super); + function Rule() { + _super.apply(this, arguments); + } + Rule.prototype.apply = function(sourceFile) { + return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions())); + }; + return Rule; +} (Lint.Rules.AbstractRule)); +exports.Rule = Rule; +function isStringLiteral(node) { + return node && node.kind === ts.SyntaxKind.StringLiteral; +} +function isObjectLiteral(node) { + return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression; +} +function isPropertyAssignment(node) { + return node && node.kind === ts.SyntaxKind.PropertyAssignment; +} +var NoUnexternalizedStringsRuleWalker = (function(_super) { + __extends(NoUnexternalizedStringsRuleWalker, _super); + function NoUnexternalizedStringsRuleWalker(file, opts) { + var _this = this; + _super.call(this, file, opts); + this.signatures = Object.create(null); + this.ignores = Object.create(null); + this.messageIndex = undefined; + this.keyIndex = undefined; + this.usedKeys = Object.create(null); + var options = this.getOptions(); + var first = options && options.length > 0 ? options[0] : null; + if (first) { + if (Array.isArray(first.signatures)) { + first.signatures.forEach(function(signature) { return _this.signatures[signature] = true; }); + } + if (Array.isArray(first.ignores)) { + first.ignores.forEach(function(ignore) { return _this.ignores[ignore] = true; }); + } + if (typeof first.messageIndex !== 'undefined') { + this.messageIndex = first.messageIndex; + } + if (typeof first.keyIndex !== 'undefined') { + this.keyIndex = first.keyIndex; + } + } + } + NoUnexternalizedStringsRuleWalker.prototype.visitSourceFile = function(node) { + var _this = this; + _super.prototype.visitSourceFile.call(this, node); + Object.keys(this.usedKeys).forEach(function(key) { + var occurences = _this.usedKeys[key]; + if (occurences.length > 1) { + occurences.forEach(function(occurence) { + _this.addFailure((_this.createFailure(occurence.key.getStart(), occurence.key.getWidth(), "Duplicate key " + occurence.key.getText() + " with different message value."))); + }); + } + }); + }; + NoUnexternalizedStringsRuleWalker.prototype.visitStringLiteral = function(node) { + this.checkStringLiteral(node); + _super.prototype.visitStringLiteral.call(this, node); + }; + NoUnexternalizedStringsRuleWalker.prototype.checkStringLiteral = function(node) { + var text = node.getText(); + var doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE; + var info = this.findDescribingParent(node); + // Ignore strings in import and export nodes. + if (info && info.ignoreUsage) { + return; + } + var callInfo = info ? info.callInfo : null; + var functionName = callInfo ? callInfo.callExpression.expression.getText() : null; + if (functionName && this.ignores[functionName]) { + return; + } + if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), "Unexternalized string found: " + node.getText())); + return; + } + // We have a single quoted string outside a localize function name. + if (!doubleQuoted && !this.signatures[functionName]) { + return; + } + // We have a string that is a direct argument into the localize call. + var keyArg = callInfo.argIndex === this.keyIndex + ? callInfo.callExpression.arguments[this.keyIndex] + : null; + if (keyArg) { + if (isStringLiteral(keyArg)) { + this.recordKey(keyArg, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined); + } + else if (isObjectLiteral(keyArg)) { + for (var i = 0; i < keyArg.properties.length; i++) { + var property = keyArg.properties[i]; + if (isPropertyAssignment(property)) { + var name_1 = property.name.getText(); + if (name_1 === 'key') { + var initializer = property.initializer; + if (isStringLiteral(initializer)) { + this.recordKey(initializer, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined); + } + break; + } + } + } + } + } + var messageArg = callInfo.argIndex === this.messageIndex + ? callInfo.callExpression.arguments[this.messageIndex] + : null; + if (messageArg && messageArg !== node) { + this.addFailure(this.createFailure(messageArg.getStart(), messageArg.getWidth(), "Message argument to '" + callInfo.callExpression.expression.getText() + "' must be a string literal.")); + return; + } + }; + NoUnexternalizedStringsRuleWalker.prototype.recordKey = function(keyNode, messageNode) { + var text = keyNode.getText(); + var occurences = this.usedKeys[text]; + if (!occurences) { + occurences = []; + this.usedKeys[text] = occurences; + } + if (messageNode) { + if (occurences.some(function(pair) { return pair.message ? pair.message.getText() === messageNode.getText() : false; })) { + return; + } + } + occurences.push({ key: keyNode, message: messageNode }); + }; + NoUnexternalizedStringsRuleWalker.prototype.findDescribingParent = function(node) { + var parent; + while ((parent = node.parent)) { + var kind = parent.kind; + if (kind === ts.SyntaxKind.CallExpression) { + var callExpression = parent; + return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } }; + } + else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) { + return { ignoreUsage: true }; + } + else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration + || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration + || kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration + || kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) { + return null; + } + node = parent; + } + }; + NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE = '"'; + return NoUnexternalizedStringsRuleWalker; +} (Lint.RuleWalker)); diff --git a/build/tslintRules/noUnexternalizedStringsRule.ts b/build/tslintRules/noUnexternalizedStringsRule.ts new file mode 100644 index 00000000000..4ae958e0a32 --- /dev/null +++ b/build/tslintRules/noUnexternalizedStringsRule.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as ts from 'typescript'; +import * as Lint from 'tslint/lib/lint'; + +/** + * Implementation of the no-unexternalized-strings rule. + */ +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions())); + } +} + +interface Map { + [key: string]: V; +} + +interface UnexternalizedStringsOptions { + signatures?: string[]; + messageIndex?: number; + keyIndex?: number; + ignores?: string[]; +} + +function isStringLiteral(node: ts.Node): node is ts.StringLiteral { + return node && node.kind === ts.SyntaxKind.StringLiteral; +} + +function isObjectLiteral(node: ts.Node): node is ts.ObjectLiteralExpression { + return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression; +} + +function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { + return node && node.kind === ts.SyntaxKind.PropertyAssignment; +} + +interface KeyMessagePair { + key: ts.StringLiteral; + message: ts.Node; +} + +class NoUnexternalizedStringsRuleWalker extends Lint.RuleWalker { + + private static DOUBLE_QUOTE: string = '"'; + + private signatures: Map; + private messageIndex: number; + private keyIndex: number; + private ignores: Map; + + private usedKeys: Map; + + constructor(file: ts.SourceFile, opts: Lint.IOptions) { + super(file, opts); + this.signatures = Object.create(null); + this.ignores = Object.create(null); + this.messageIndex = undefined; + this.keyIndex = undefined; + this.usedKeys = Object.create(null); + let options: any[] = this.getOptions(); + let first: UnexternalizedStringsOptions = options && options.length > 0 ? options[0] : null; + if (first) { + if (Array.isArray(first.signatures)) { + first.signatures.forEach((signature: string) => this.signatures[signature] = true); + } + if (Array.isArray(first.ignores)) { + first.ignores.forEach((ignore: string) => this.ignores[ignore] = true); + } + if (typeof first.messageIndex !== 'undefined') { + this.messageIndex = first.messageIndex; + } + if (typeof first.keyIndex !== 'undefined') { + this.keyIndex = first.keyIndex; + } + } + } + + protected visitSourceFile(node: ts.SourceFile): void { + super.visitSourceFile(node); + Object.keys(this.usedKeys).forEach(key => { + let occurences = this.usedKeys[key]; + if (occurences.length > 1) { + occurences.forEach(occurence => { + this.addFailure((this.createFailure(occurence.key.getStart(), occurence.key.getWidth(), `Duplicate key ${occurence.key.getText()} with different message value.`))); + }); + } + }); + } + + protected visitStringLiteral(node: ts.StringLiteral): void { + this.checkStringLiteral(node); + super.visitStringLiteral(node); + } + + private checkStringLiteral(node: ts.StringLiteral): void { + let text = node.getText(); + let doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE; + let info = this.findDescribingParent(node); + // Ignore strings in import and export nodes. + if (info && info.ignoreUsage) { + return; + } + let callInfo = info ? info.callInfo : null; + let functionName = callInfo ? callInfo.callExpression.expression.getText() : null; + if (functionName && this.ignores[functionName]) { + return; + } + if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Unexternalized string found: ${node.getText()}`)); + return; + } + // We have a single quoted string outside a localize function name. + if (!doubleQuoted && !this.signatures[functionName]) { + return; + } + // We have a string that is a direct argument into the localize call. + let keyArg: ts.Expression = callInfo.argIndex === this.keyIndex + ? callInfo.callExpression.arguments[this.keyIndex] + : null; + if (keyArg) { + if (isStringLiteral(keyArg)) { + this.recordKey(keyArg, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined); + } else if (isObjectLiteral(keyArg)) { + for (let i = 0; i < keyArg.properties.length; i++) { + let property = keyArg.properties[i]; + if (isPropertyAssignment(property)) { + let name = property.name.getText(); + if (name === 'key') { + let initializer = property.initializer; + if (isStringLiteral(initializer)) { + this.recordKey(initializer, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined); + } + break; + } + } + } + } + } + let messageArg: ts.Expression = callInfo.argIndex === this.messageIndex + ? callInfo.callExpression.arguments[this.messageIndex] + : null; + if (messageArg && messageArg !== node) { + this.addFailure(this.createFailure( + messageArg.getStart(), messageArg.getWidth(), + `Message argument to '${callInfo.callExpression.expression.getText()}' must be a string literal.`)); + return; + } + } + + private recordKey(keyNode: ts.StringLiteral, messageNode: ts.Node) { + let text = keyNode.getText(); + let occurences: KeyMessagePair[] = this.usedKeys[text]; + if (!occurences) { + occurences = []; + this.usedKeys[text] = occurences; + } + if (messageNode) { + if (occurences.some(pair => pair.message ? pair.message.getText() === messageNode.getText() : false)) { + return; + } + } + occurences.push({ key: keyNode, message: messageNode }); + } + + private findDescribingParent(node: ts.Node): { callInfo?: { callExpression: ts.CallExpression, argIndex: number }, ignoreUsage?: boolean; } { + let parent: ts.Node; + while ((parent = node.parent)) { + let kind = parent.kind; + if (kind === ts.SyntaxKind.CallExpression) { + let callExpression = parent as ts.CallExpression; + return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } }; + } else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) { + return { ignoreUsage: true }; + } else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration + || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration + || kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration + || kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) { + return null; + } + node = parent; + } + } +} \ No newline at end of file diff --git a/build/tslintRules/tsconfig.json b/build/tslintRules/tsconfig.json new file mode 100644 index 00000000000..6b7a334be71 --- /dev/null +++ b/build/tslintRules/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "moduleResolution": "node", + "newLine": "LF" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 39822acbb65..25c1a77c9af 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "sinon": "^1.17.2", "source-map": "^0.4.4", "tslint": "^3.2.2", - "tslint-microsoft-contrib": "^2.0.0", "typescript": "^1.8.0", "uglify-js": "2.4.8", "underscore": "^1.8.2", diff --git a/tslint.json b/tslint.json index 98135425d75..4ea88cdbb9b 100644 --- a/tslint.json +++ b/tslint.json @@ -8,6 +8,14 @@ "curly": true, "class-name": true, "semicolon": true, - "triple-equals": true + "triple-equals": true, + "no-unexternalized-strings": [ + true, + { + "signatures": ["localize", "nls.localize"], + "keyIndex": 0, + "messageIndex": 1 + } + ] } } \ No newline at end of file