Skip to content

Commit d47c2c0

Browse files
committed
Port noUnexternalizedStringsRule to TS 1.8.0. Add support to detect duplicate keys with different messages
1 parent c3d6ebc commit d47c2c0

7 files changed

Lines changed: 373 additions & 4 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
"extensions/**/out/**": true
1414
},
1515
"tslint.enable": true,
16-
"tslint.rulesDirectory": "node_modules/tslint-microsoft-contrib"
16+
"tslint.rulesDirectory": "build/tslintRules"
1717
}

build/gulpfile.hygiene.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ var lintReporter = function (output, file, options) {
104104
gulp.task('tslint', function () {
105105
return gulp.src(all, { base: '.' })
106106
.pipe(filter(tslintFilter))
107-
.pipe(tslint({ rulesDirectory: 'node_modules/tslint-microsoft-contrib' }))
107+
.pipe(tslint({ rulesDirectory: 'build/tslintRules' }))
108108
.pipe(tslint.report(lintReporter, {
109109
summarizeFailureOutput: false,
110110
emitError: false
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
"use strict";
6+
var __extends = (this && this.__extends) || function(d, b) {
7+
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
8+
function __() { this.constructor = d; }
9+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
10+
};
11+
var ts = require('typescript');
12+
var Lint = require('tslint/lib/lint');
13+
/**
14+
* Implementation of the no-unexternalized-strings rule.
15+
*/
16+
var Rule = (function(_super) {
17+
__extends(Rule, _super);
18+
function Rule() {
19+
_super.apply(this, arguments);
20+
}
21+
Rule.prototype.apply = function(sourceFile) {
22+
return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions()));
23+
};
24+
return Rule;
25+
} (Lint.Rules.AbstractRule));
26+
exports.Rule = Rule;
27+
function isStringLiteral(node) {
28+
return node && node.kind === ts.SyntaxKind.StringLiteral;
29+
}
30+
function isObjectLiteral(node) {
31+
return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression;
32+
}
33+
function isPropertyAssignment(node) {
34+
return node && node.kind === ts.SyntaxKind.PropertyAssignment;
35+
}
36+
var NoUnexternalizedStringsRuleWalker = (function(_super) {
37+
__extends(NoUnexternalizedStringsRuleWalker, _super);
38+
function NoUnexternalizedStringsRuleWalker(file, opts) {
39+
var _this = this;
40+
_super.call(this, file, opts);
41+
this.signatures = Object.create(null);
42+
this.ignores = Object.create(null);
43+
this.messageIndex = undefined;
44+
this.keyIndex = undefined;
45+
this.usedKeys = Object.create(null);
46+
var options = this.getOptions();
47+
var first = options && options.length > 0 ? options[0] : null;
48+
if (first) {
49+
if (Array.isArray(first.signatures)) {
50+
first.signatures.forEach(function(signature) { return _this.signatures[signature] = true; });
51+
}
52+
if (Array.isArray(first.ignores)) {
53+
first.ignores.forEach(function(ignore) { return _this.ignores[ignore] = true; });
54+
}
55+
if (typeof first.messageIndex !== 'undefined') {
56+
this.messageIndex = first.messageIndex;
57+
}
58+
if (typeof first.keyIndex !== 'undefined') {
59+
this.keyIndex = first.keyIndex;
60+
}
61+
}
62+
}
63+
NoUnexternalizedStringsRuleWalker.prototype.visitSourceFile = function(node) {
64+
var _this = this;
65+
_super.prototype.visitSourceFile.call(this, node);
66+
Object.keys(this.usedKeys).forEach(function(key) {
67+
var occurences = _this.usedKeys[key];
68+
if (occurences.length > 1) {
69+
occurences.forEach(function(occurence) {
70+
_this.addFailure((_this.createFailure(occurence.key.getStart(), occurence.key.getWidth(), "Duplicate key " + occurence.key.getText() + " with different message value.")));
71+
});
72+
}
73+
});
74+
};
75+
NoUnexternalizedStringsRuleWalker.prototype.visitStringLiteral = function(node) {
76+
this.checkStringLiteral(node);
77+
_super.prototype.visitStringLiteral.call(this, node);
78+
};
79+
NoUnexternalizedStringsRuleWalker.prototype.checkStringLiteral = function(node) {
80+
var text = node.getText();
81+
var doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE;
82+
var info = this.findDescribingParent(node);
83+
// Ignore strings in import and export nodes.
84+
if (info && info.ignoreUsage) {
85+
return;
86+
}
87+
var callInfo = info ? info.callInfo : null;
88+
var functionName = callInfo ? callInfo.callExpression.expression.getText() : null;
89+
if (functionName && this.ignores[functionName]) {
90+
return;
91+
}
92+
if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) {
93+
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), "Unexternalized string found: " + node.getText()));
94+
return;
95+
}
96+
// We have a single quoted string outside a localize function name.
97+
if (!doubleQuoted && !this.signatures[functionName]) {
98+
return;
99+
}
100+
// We have a string that is a direct argument into the localize call.
101+
var keyArg = callInfo.argIndex === this.keyIndex
102+
? callInfo.callExpression.arguments[this.keyIndex]
103+
: null;
104+
if (keyArg) {
105+
if (isStringLiteral(keyArg)) {
106+
this.recordKey(keyArg, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
107+
}
108+
else if (isObjectLiteral(keyArg)) {
109+
for (var i = 0; i < keyArg.properties.length; i++) {
110+
var property = keyArg.properties[i];
111+
if (isPropertyAssignment(property)) {
112+
var name_1 = property.name.getText();
113+
if (name_1 === 'key') {
114+
var initializer = property.initializer;
115+
if (isStringLiteral(initializer)) {
116+
this.recordKey(initializer, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
117+
}
118+
break;
119+
}
120+
}
121+
}
122+
}
123+
}
124+
var messageArg = callInfo.argIndex === this.messageIndex
125+
? callInfo.callExpression.arguments[this.messageIndex]
126+
: null;
127+
if (messageArg && messageArg !== node) {
128+
this.addFailure(this.createFailure(messageArg.getStart(), messageArg.getWidth(), "Message argument to '" + callInfo.callExpression.expression.getText() + "' must be a string literal."));
129+
return;
130+
}
131+
};
132+
NoUnexternalizedStringsRuleWalker.prototype.recordKey = function(keyNode, messageNode) {
133+
var text = keyNode.getText();
134+
var occurences = this.usedKeys[text];
135+
if (!occurences) {
136+
occurences = [];
137+
this.usedKeys[text] = occurences;
138+
}
139+
if (messageNode) {
140+
if (occurences.some(function(pair) { return pair.message ? pair.message.getText() === messageNode.getText() : false; })) {
141+
return;
142+
}
143+
}
144+
occurences.push({ key: keyNode, message: messageNode });
145+
};
146+
NoUnexternalizedStringsRuleWalker.prototype.findDescribingParent = function(node) {
147+
var parent;
148+
while ((parent = node.parent)) {
149+
var kind = parent.kind;
150+
if (kind === ts.SyntaxKind.CallExpression) {
151+
var callExpression = parent;
152+
return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } };
153+
}
154+
else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) {
155+
return { ignoreUsage: true };
156+
}
157+
else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration
158+
|| kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration
159+
|| kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration
160+
|| kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) {
161+
return null;
162+
}
163+
node = parent;
164+
}
165+
};
166+
NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE = '"';
167+
return NoUnexternalizedStringsRuleWalker;
168+
} (Lint.RuleWalker));
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as ts from 'typescript';
6+
import * as Lint from 'tslint/lib/lint';
7+
8+
/**
9+
* Implementation of the no-unexternalized-strings rule.
10+
*/
11+
export class Rule extends Lint.Rules.AbstractRule {
12+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
13+
return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions()));
14+
}
15+
}
16+
17+
interface Map<V> {
18+
[key: string]: V;
19+
}
20+
21+
interface UnexternalizedStringsOptions {
22+
signatures?: string[];
23+
messageIndex?: number;
24+
keyIndex?: number;
25+
ignores?: string[];
26+
}
27+
28+
function isStringLiteral(node: ts.Node): node is ts.StringLiteral {
29+
return node && node.kind === ts.SyntaxKind.StringLiteral;
30+
}
31+
32+
function isObjectLiteral(node: ts.Node): node is ts.ObjectLiteralExpression {
33+
return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression;
34+
}
35+
36+
function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment {
37+
return node && node.kind === ts.SyntaxKind.PropertyAssignment;
38+
}
39+
40+
interface KeyMessagePair {
41+
key: ts.StringLiteral;
42+
message: ts.Node;
43+
}
44+
45+
class NoUnexternalizedStringsRuleWalker extends Lint.RuleWalker {
46+
47+
private static DOUBLE_QUOTE: string = '"';
48+
49+
private signatures: Map<boolean>;
50+
private messageIndex: number;
51+
private keyIndex: number;
52+
private ignores: Map<boolean>;
53+
54+
private usedKeys: Map<KeyMessagePair[]>;
55+
56+
constructor(file: ts.SourceFile, opts: Lint.IOptions) {
57+
super(file, opts);
58+
this.signatures = Object.create(null);
59+
this.ignores = Object.create(null);
60+
this.messageIndex = undefined;
61+
this.keyIndex = undefined;
62+
this.usedKeys = Object.create(null);
63+
let options: any[] = this.getOptions();
64+
let first: UnexternalizedStringsOptions = options && options.length > 0 ? options[0] : null;
65+
if (first) {
66+
if (Array.isArray(first.signatures)) {
67+
first.signatures.forEach((signature: string) => this.signatures[signature] = true);
68+
}
69+
if (Array.isArray(first.ignores)) {
70+
first.ignores.forEach((ignore: string) => this.ignores[ignore] = true);
71+
}
72+
if (typeof first.messageIndex !== 'undefined') {
73+
this.messageIndex = first.messageIndex;
74+
}
75+
if (typeof first.keyIndex !== 'undefined') {
76+
this.keyIndex = first.keyIndex;
77+
}
78+
}
79+
}
80+
81+
protected visitSourceFile(node: ts.SourceFile): void {
82+
super.visitSourceFile(node);
83+
Object.keys(this.usedKeys).forEach(key => {
84+
let occurences = this.usedKeys[key];
85+
if (occurences.length > 1) {
86+
occurences.forEach(occurence => {
87+
this.addFailure((this.createFailure(occurence.key.getStart(), occurence.key.getWidth(), `Duplicate key ${occurence.key.getText()} with different message value.`)));
88+
});
89+
}
90+
});
91+
}
92+
93+
protected visitStringLiteral(node: ts.StringLiteral): void {
94+
this.checkStringLiteral(node);
95+
super.visitStringLiteral(node);
96+
}
97+
98+
private checkStringLiteral(node: ts.StringLiteral): void {
99+
let text = node.getText();
100+
let doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE;
101+
let info = this.findDescribingParent(node);
102+
// Ignore strings in import and export nodes.
103+
if (info && info.ignoreUsage) {
104+
return;
105+
}
106+
let callInfo = info ? info.callInfo : null;
107+
let functionName = callInfo ? callInfo.callExpression.expression.getText() : null;
108+
if (functionName && this.ignores[functionName]) {
109+
return;
110+
}
111+
if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) {
112+
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Unexternalized string found: ${node.getText()}`));
113+
return;
114+
}
115+
// We have a single quoted string outside a localize function name.
116+
if (!doubleQuoted && !this.signatures[functionName]) {
117+
return;
118+
}
119+
// We have a string that is a direct argument into the localize call.
120+
let keyArg: ts.Expression = callInfo.argIndex === this.keyIndex
121+
? callInfo.callExpression.arguments[this.keyIndex]
122+
: null;
123+
if (keyArg) {
124+
if (isStringLiteral(keyArg)) {
125+
this.recordKey(keyArg, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
126+
} else if (isObjectLiteral(keyArg)) {
127+
for (let i = 0; i < keyArg.properties.length; i++) {
128+
let property = keyArg.properties[i];
129+
if (isPropertyAssignment(property)) {
130+
let name = property.name.getText();
131+
if (name === 'key') {
132+
let initializer = property.initializer;
133+
if (isStringLiteral(initializer)) {
134+
this.recordKey(initializer, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
135+
}
136+
break;
137+
}
138+
}
139+
}
140+
}
141+
}
142+
let messageArg: ts.Expression = callInfo.argIndex === this.messageIndex
143+
? callInfo.callExpression.arguments[this.messageIndex]
144+
: null;
145+
if (messageArg && messageArg !== node) {
146+
this.addFailure(this.createFailure(
147+
messageArg.getStart(), messageArg.getWidth(),
148+
`Message argument to '${callInfo.callExpression.expression.getText()}' must be a string literal.`));
149+
return;
150+
}
151+
}
152+
153+
private recordKey(keyNode: ts.StringLiteral, messageNode: ts.Node) {
154+
let text = keyNode.getText();
155+
let occurences: KeyMessagePair[] = this.usedKeys[text];
156+
if (!occurences) {
157+
occurences = [];
158+
this.usedKeys[text] = occurences;
159+
}
160+
if (messageNode) {
161+
if (occurences.some(pair => pair.message ? pair.message.getText() === messageNode.getText() : false)) {
162+
return;
163+
}
164+
}
165+
occurences.push({ key: keyNode, message: messageNode });
166+
}
167+
168+
private findDescribingParent(node: ts.Node): { callInfo?: { callExpression: ts.CallExpression, argIndex: number }, ignoreUsage?: boolean; } {
169+
let parent: ts.Node;
170+
while ((parent = node.parent)) {
171+
let kind = parent.kind;
172+
if (kind === ts.SyntaxKind.CallExpression) {
173+
let callExpression = parent as ts.CallExpression;
174+
return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(<any>node) } };
175+
} else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) {
176+
return { ignoreUsage: true };
177+
} else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration
178+
|| kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration
179+
|| kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration
180+
|| kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) {
181+
return null;
182+
}
183+
node = parent;
184+
}
185+
}
186+
}

build/tslintRules/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es5",
5+
"moduleResolution": "node",
6+
"newLine": "LF"
7+
}
8+
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@
8383
"sinon": "^1.17.2",
8484
"source-map": "^0.4.4",
8585
"tslint": "^3.2.2",
86-
"tslint-microsoft-contrib": "^2.0.0",
8786
"typescript": "^1.8.0",
8887
"uglify-js": "2.4.8",
8988
"underscore": "^1.8.2",

0 commit comments

Comments
 (0)