diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4e91cf1c1..36d73e6ab 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,6 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.insertSpaces": true,
- "editor.tabSize": 4
+ "editor.tabSize": 4,
+ "files.trimTrailingWhitespace": true
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 25d26165d..07180d44d 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ A set of [TSLint](https://github.com/palantir/tslint) rules used on some Microso
Version 2.0.1
-------------
-The project has been in use for at least several months on multiple projects. Please report any bugs or false positives you might find!
+The project has been in use for at least several months on multiple projects. Please report any bugs or false positives you might find!
**TSLint version 3.2.x users**: use project tslint-microsoft-contrib version 2.x
**TSLint version 3.1.x users**: Unsupported
@@ -22,7 +22,7 @@ Installation
npm install tslint-microsoft-contrib
-Alternately, you can download the files directly from GitHub:
+Alternately, you can download the files directly from GitHub:
* [Latest Development Version](https://github.com/Microsoft/tslint-microsoft-contrib/tree/releases)
* [2.0.0](https://github.com/Microsoft/tslint-microsoft-contrib/tree/npm-2.0.0)
@@ -101,6 +101,18 @@ Rule Name | Description | Since
`react-no-dangerous-html` | Do not use React's dangerouslySetInnerHTML API. This rule finds usages of the dangerouslySetInnerHTML API (but not any JSX references). For more info see the [react-no-dangerous-html Rule wiki page](https://github.com/Microsoft/tslint-microsoft-contrib/wiki/react-no-dangerous-html-Rule). | 0.0.2
`use-named-parameter` | Do not reference the arguments object by numerical index; instead, use a named parameter. This rule is similar to JSLint's [Use a named parameter](https://jslinterrors.com/use-a-named-parameter) rule. | 0.0.3
`valid-typeof` | Ensures that the results of typeof are compared against a valid string. This rule aims to prevent errors from likely typos by ensuring that when the result of a typeof operation is compared against a string, that the string is a valid value. Similar to the [valid-typeof ESLint rule](http://eslint.org/docs/rules/valid-typeof).| 1.0
+`no-unexternalized-strings` | Ensures that double quoted strings are passed to a localize call to provide proper strings for different locales. The rule can be configured using the following object literal:
+```typescript
+{
+ /** list function signatures that localize a string */
+ "signature": string[];
+ /** defines which argument denotes the message */
+ "messageIndex": number;
+ /** list signatures which are ignored when double quoted strings */
+ "ignores": string[]
+}
+```
+| 2.0.2
Development
diff --git a/src/noUnexternalizedStringsRule.ts b/src/noUnexternalizedStringsRule.ts
new file mode 100644
index 000000000..05babc747
--- /dev/null
+++ b/src/noUnexternalizedStringsRule.ts
@@ -0,0 +1,113 @@
+import * as ts from 'typescript';
+import * as Lint from 'tslint/lib/lint';
+
+import ErrorTolerantWalker = require('./utils/ErrorTolerantWalker');
+import SyntaxKind = require('./utils/SyntaxKind');
+
+/**
+ * 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;
+ ignores?: string[];
+}
+
+class NoUnexternalizedStringsRuleWalker extends ErrorTolerantWalker {
+
+ private static SINGLE_QUOTE: string = '\'';
+ private static DOUBLE_QUOTE: string = '"';
+
+ private signatures: Map;
+ private messageIndex: number;
+ private ignores: Map;
+
+ constructor(sourceFile: ts.SourceFile, opt: Lint.IOptions) {
+ super(sourceFile, opt);
+ this.signatures = Object.create(null);
+ this.ignores = Object.create(null);
+ this.messageIndex = undefined;
+ 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;
+ }
+ }
+ }
+
+
+ protected visitStringLiteral(node: ts.StringLiteral): void {
+ this.checkStringLiteral(node);
+ super.visitStringLiteral(node);
+ }
+
+ private checkStringLiteral(node: ts.StringLiteral): void {
+ let text = node.getText();
+ // The string literal is enclosed in single quotes. Treat as OK.
+ if (text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.SINGLE_QUOTE
+ && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.SINGLE_QUOTE) {
+ return;
+ }
+ let info = this.findDescribingParent(node);
+ // Ignore strings in import and export nodes.
+ if (info && info.ignoreUsage) {
+ return;
+ }
+ let callInfo = info ? info.callInfo : null;
+ if (callInfo && this.ignores[callInfo.callExpression.expression.getText()]) {
+ return;
+ }
+ if (!callInfo || callInfo.argIndex === -1 || !this.signatures[callInfo.callExpression.expression.getText()]) {
+ this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Unexternalized string found: ${node.getText()}`));
+ return;
+ }
+ // We have a string that is a direct argument into the localize call.
+ 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 findDescribingParent(node: ts.Node):
+ { callInfo?: { callExpression: ts.CallExpression, argIndex: number }, ignoreUsage?: boolean; } {
+ let kinds = SyntaxKind.current();
+ let parent: ts.Node;
+ while ((parent = node.parent)) {
+ let kind = parent.kind;
+ if (kind === kinds.CallExpression) {
+ let callExpression = parent as ts.CallExpression;
+ return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) }};
+ } else if (kind === kinds.ImportEqualsDeclaration || kind === kinds.ImportDeclaration || kind === kinds.ExportDeclaration) {
+ return { ignoreUsage: true };
+ } else if (kind === kinds.VariableDeclaration || kind === kinds.FunctionDeclaration || kind === kinds.PropertyDeclaration
+ || kind === kinds.MethodDeclaration || kind === kinds.VariableDeclarationList || kind === kinds.InterfaceDeclaration
+ || kind === kinds.ClassDeclaration || kind === kinds.EnumDeclaration || kind === kinds.ModuleDeclaration
+ || kind === kinds.TypeAliasDeclaration || kind === kinds.SourceFile) {
+ return null;
+ }
+ node = parent;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/NoUnexternalizedStringsRuleTests.ts b/tests/NoUnexternalizedStringsRuleTests.ts
new file mode 100644
index 000000000..a9571b786
--- /dev/null
+++ b/tests/NoUnexternalizedStringsRuleTests.ts
@@ -0,0 +1,176 @@
+///
+///
+
+/* tslint:disable:quotemark */
+/* tslint:disable:no-multiline-string */
+
+import TestHelper = require('./TestHelper');
+
+/**
+ * Unit tests.
+ */
+describe('noUnexternalizedStringsRule', () : void => {
+
+ var ruleName : string = 'no-unexternalized-strings';
+
+ it('should pass on single quote', () : void => {
+ var script : string = `
+ let str = 'Hello Worlds';
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on template expression', () : void => {
+ var script : string = 'let str = `Hello ${var} Worlds`;';
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on localize', () : void => {
+ var script : string = `
+ let str = localize("key", "Hello Worlds");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on nls.localize', () : void => {
+ var script : string = `
+ import nls = require('nls');
+ let str = nls.localize("Key", "Hello World");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on import', () : void => {
+ var script : string = `
+ import { localize } from "nls";
+ let str = localize("Key", "Hello World");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on import equals', () : void => {
+ var script : string = `
+ import nls = require("nls");
+ let str = nls.localize("Key", "Hello World");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [ ]);
+ });
+
+ it('should pass on ignores', () : void => {
+ var script : string = `
+ var nls = require("nls");
+ let str = nls.localize("Key", "Hello World");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName,
+ [{ signatures: ['localize', 'nls.localize'], messageIndex: 1, ignores: ['require'] }],
+ script, [ ]
+ );
+ });
+
+ it('should fail on my.localize', () : void => {
+ var script : string = `
+ let str = my.localize('key', "Needs localization");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Unexternalized string found: \"Needs localization\"",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 42,
+ "line": 2
+ }
+ }
+ ]);
+ });
+
+ it('should fail on function call inside localize', () : void => {
+ var script : string = `
+ let str = localize('key', foo("Needs localization"));
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Unexternalized string found: \"Needs localization\"",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 43,
+ "line": 2
+ }
+ }
+ ]);
+ });
+
+ it('should fail on method call inside localize', () : void => {
+ var script : string = `
+ let str = localize('key', this.foo("Needs localization"));
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Unexternalized string found: \"Needs localization\"",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 48,
+ "line": 2
+ }
+ }
+ ]);
+ });
+
+ it('should fail on variable declaration', () : void => {
+ var script : string = `
+ let str = "Needs localization";
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Unexternalized string found: \"Needs localization\"",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 23,
+ "line": 2
+ }
+ }
+ ]);
+ });
+
+ it('should fail on function declaration', () : void => {
+ var script : string = `
+ let str: string = undefined;
+ function foo() {
+ str = "Hello World";
+ }
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Unexternalized string found: \"Hello World\"",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 23,
+ "line": 4
+ }
+ }
+ ]);
+ });
+
+ it('should fail on binary expression', () : void => {
+ var script : string = `
+ localize('key', "Hello " + "World");
+ `;
+ TestHelper.assertViolationsWithOptions(ruleName, [{ signatures: ['localize', 'nls.localize'], messageIndex: 1 }], script, [
+ {
+ "failure": "Message argument to 'localize' must be a string literal.",
+ "name": "file.ts",
+ "ruleName": "no-unexternalized-strings",
+ "startPosition": {
+ "character": 29,
+ "line": 2
+ }
+ }
+ ]);
+ });
+});
+/* tslint:enable:quotemark */
+/* tslint:enable:no-multiline-string */
\ No newline at end of file