From 53510789bcbae28cc2e8b18bb23b8a2bd2f2e447 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Wed, 20 Jan 2016 11:02:55 +0100 Subject: [PATCH] [Issue #95] new rule: no-unexternalized-strings Updated readme.md Add support to ingore rules during 'validate-config' closes #95 --- .vscode/settings.json | 3 +- Gruntfile.js | 4 + README.md | 5 +- src/noUnexternalizedStringsRule.ts | 113 ++++++++++++++ tests/NoUnexternalizedStringsRuleTests.ts | 176 ++++++++++++++++++++++ 5 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 src/noUnexternalizedStringsRule.ts create mode 100644 tests/NoUnexternalizedStringsRuleTests.ts 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/Gruntfile.js b/Gruntfile.js index cc51df29d..204119932 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -138,7 +138,11 @@ module.exports = function(grunt) { ' during the build.', function () { var tslintConfig = grunt.file.readJSON('tslint.json', { encoding: 'UTF-8' }); + var rulesToSkip = { 'no-unexternalized-strings': true }; getAllRuleNames().forEach(function(ruleName) { + if (rulesToSkip[ruleName]) { + return; + } if (tslintConfig.rules[ruleName] !== true) { if (tslintConfig.rules[ruleName] == null || tslintConfig.rules[ruleName][0] !== true) { grunt.fail.warn('A rule was found that is not enabled on the project: ' + ruleName); diff --git a/README.md b/README.md index 25d26165d..e883ae176 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,7 @@ 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 an object literal as document in the [feature request](https://github.com/Microsoft/tslint-microsoft-contrib/issues/95#issuecomment-173149989)| 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