Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Commit

Permalink
[Issue #95] new rule: no-unexternalized-strings
Browse files Browse the repository at this point in the history
Updated readme.md

Add support to ingore rules during 'validate-config'

closes #95
  • Loading branch information
dbaeumer authored and HamletDRC committed Jan 20, 2016
1 parent b811289 commit 5351078
Show file tree
Hide file tree
Showing 5 changed files with 298 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
}
4 changes: 4 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 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,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
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 5351078

Please sign in to comment.