Skip to content

Commit

Permalink
Implement microsoft#95: new rule: no-unexternalized-strings
Browse files Browse the repository at this point in the history
  • Loading branch information
dbaeumer committed Jan 20, 2016
1 parent f03dd45 commit 0ce1ef7
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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:<br>
```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
Expand Down
113 changes: 113 additions & 0 deletions src/noUnexternalizedStringsRule.ts
Original file line number Diff line number Diff line change
@@ -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<V> {
[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<boolean>;
private messageIndex: number;
private ignores: Map<boolean>;

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(<any>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;
}
}
}
176 changes: 176 additions & 0 deletions tests/NoUnexternalizedStringsRuleTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/// <reference path="../typings/mocha.d.ts" />
/// <reference path="../typings/chai.d.ts" />

/* 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 */

0 comments on commit 0ce1ef7

Please sign in to comment.