Skip to content

Commit a61f38e

Browse files
committed
migrate no-unexternalized-strings rule (have two variants)
1 parent 53d1dff commit a61f38e

File tree

5 files changed

+706
-0
lines changed

5 files changed

+706
-0
lines changed

.eslintrc.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@
3131
"code-translation-remind": "warn",
3232
"code-no-nls-in-standalone-editor": "warn",
3333
"code-no-standalone-editor": "warn",
34+
"code-no-unexternalized-strings2": "warn",
35+
"code-no-unexternalized-strings": [
36+
"off",
37+
{
38+
"signatures": [
39+
"localize",
40+
"nls.localize"
41+
],
42+
"keyIndex": 0,
43+
"messageIndex": 1
44+
}
45+
],
3446
"code-layering": [
3547
"warn",
3648
{
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"use strict";
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
var _a;
7+
const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
8+
function isStringLiteral(node) {
9+
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string';
10+
}
11+
function isObjectLiteral(node) {
12+
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression;
13+
}
14+
function isPropertyAssignment(node) {
15+
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Property;
16+
}
17+
module.exports = new (_a = class NoUnexternalizedStringsRuleWalker {
18+
constructor() {
19+
this.signatures = Object.create(null);
20+
this.ignores = Object.create(null);
21+
this.usedKeys = Object.create(null);
22+
this.meta = {
23+
type: 'problem',
24+
schema: {},
25+
messages: {
26+
badQuotes: 'Do not use double quotes for imports.',
27+
unexternalized: 'Unexternalized string.',
28+
duplicateKey: `Duplicate key '{{key}}' with different message value.`,
29+
badKey: `The key {{key}} doesn't conform to a valid localize identifier`,
30+
emptyKey: 'Key is empty.',
31+
whitespaceKey: 'Key is only whitespace.',
32+
badMessage: `Message argument to '{{message}}' must be a string literal.`
33+
}
34+
};
35+
}
36+
create(context) {
37+
const first = context.options[0];
38+
if (first) {
39+
if (Array.isArray(first.signatures)) {
40+
first.signatures.forEach((signature) => this.signatures[signature] = true);
41+
}
42+
if (Array.isArray(first.ignores)) {
43+
first.ignores.forEach((ignore) => this.ignores[ignore] = true);
44+
}
45+
if (typeof first.messageIndex !== 'undefined') {
46+
this.messageIndex = first.messageIndex;
47+
}
48+
if (typeof first.keyIndex !== 'undefined') {
49+
this.keyIndex = first.keyIndex;
50+
}
51+
}
52+
return {
53+
['Program:exit']: () => {
54+
this._checkProgramEnd(context);
55+
},
56+
['Literal']: (node) => {
57+
if (typeof node.value === 'string') {
58+
this._checkStringLiteral(context, node);
59+
}
60+
},
61+
};
62+
}
63+
_checkProgramEnd(context) {
64+
Object.keys(this.usedKeys).forEach(key => {
65+
// Keys are quoted.
66+
const identifier = key.substr(1, key.length - 2);
67+
const occurrences = this.usedKeys[key];
68+
// bad key
69+
if (!NoUnexternalizedStringsRuleWalker.IDENTIFIER.test(identifier)) {
70+
context.report({
71+
loc: occurrences[0].key.loc,
72+
messageId: 'badKey',
73+
data: { key: occurrences[0].key.value }
74+
});
75+
}
76+
// duplicates key
77+
if (occurrences.length > 1) {
78+
occurrences.forEach(occurrence => {
79+
context.report({
80+
loc: occurrence.key.loc,
81+
messageId: 'duplicateKey',
82+
data: { key: occurrence.key.value }
83+
});
84+
});
85+
}
86+
});
87+
}
88+
_checkStringLiteral(context, node) {
89+
var _a;
90+
const text = node.raw;
91+
const doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE;
92+
const info = this._findDescribingParent(node);
93+
// Ignore strings in import and export nodes.
94+
if (info && info.isImport && doubleQuoted) {
95+
context.report({
96+
loc: node.loc,
97+
messageId: 'badQuotes'
98+
});
99+
return;
100+
}
101+
const callInfo = info ? info.callInfo : null;
102+
const functionName = callInfo && isStringLiteral(callInfo.callExpression.callee)
103+
? callInfo.callExpression.callee.value
104+
: null;
105+
if (functionName && this.ignores[functionName]) {
106+
return;
107+
}
108+
if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) {
109+
context.report({
110+
loc: node.loc,
111+
messageId: 'unexternalized'
112+
});
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+
const keyArg = callInfo && callInfo.argIndex === this.keyIndex
121+
? callInfo.callExpression.arguments[this.keyIndex]
122+
: null;
123+
if (keyArg) {
124+
if (isStringLiteral(keyArg)) {
125+
this.recordKey(context, keyArg, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
126+
}
127+
else if (isObjectLiteral(keyArg)) {
128+
for (const property of keyArg.properties) {
129+
if (isPropertyAssignment(property)) {
130+
const name = NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), property.key);
131+
if (name === 'key') {
132+
const initializer = property.value;
133+
if (isStringLiteral(initializer)) {
134+
this.recordKey(context, initializer, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
135+
}
136+
break;
137+
}
138+
}
139+
}
140+
}
141+
}
142+
const messageArg = callInfo.callExpression.arguments[this.messageIndex];
143+
if (messageArg && !isStringLiteral(messageArg)) {
144+
context.report({
145+
loc: messageArg.loc,
146+
messageId: 'badMessage',
147+
data: { message: NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), (_a = callInfo) === null || _a === void 0 ? void 0 : _a.callExpression.callee) }
148+
});
149+
return;
150+
}
151+
}
152+
recordKey(context, keyNode, messageNode) {
153+
const text = keyNode.raw;
154+
// We have an empty key
155+
if (text.match(/(['"]) *\1/)) {
156+
if (messageNode) {
157+
context.report({
158+
loc: keyNode.loc,
159+
messageId: 'whitespaceKey'
160+
});
161+
}
162+
else {
163+
context.report({
164+
loc: keyNode.loc,
165+
messageId: 'emptyKey'
166+
});
167+
}
168+
return;
169+
}
170+
let occurrences = this.usedKeys[text];
171+
if (!occurrences) {
172+
occurrences = [];
173+
this.usedKeys[text] = occurrences;
174+
}
175+
if (messageNode) {
176+
if (occurrences.some(pair => pair.message ? NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), pair.message) === NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), messageNode) : false)) {
177+
return;
178+
}
179+
}
180+
occurrences.push({ key: keyNode, message: messageNode });
181+
}
182+
_findDescribingParent(node) {
183+
let parent;
184+
while ((parent = node.parent)) {
185+
const kind = parent.type;
186+
if (kind === experimental_utils_1.AST_NODE_TYPES.CallExpression) {
187+
const callExpression = parent;
188+
return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } };
189+
}
190+
else if (kind === experimental_utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.ImportDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
191+
return { isImport: true };
192+
}
193+
else if (kind === experimental_utils_1.AST_NODE_TYPES.VariableDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.FunctionDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.TSPropertySignature
194+
|| kind === experimental_utils_1.AST_NODE_TYPES.TSMethodSignature || kind === experimental_utils_1.AST_NODE_TYPES.TSInterfaceDeclaration
195+
|| kind === experimental_utils_1.AST_NODE_TYPES.ClassDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.TSEnumDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.TSModuleDeclaration
196+
|| kind === experimental_utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.Program) {
197+
return null;
198+
}
199+
node = parent;
200+
}
201+
return null;
202+
}
203+
static _getText(source, node) {
204+
if (node.type === experimental_utils_1.AST_NODE_TYPES.Literal) {
205+
return String(node.value);
206+
}
207+
const start = source.getIndexFromLoc(node.loc.start);
208+
const end = source.getIndexFromLoc(node.loc.end);
209+
return source.getText().substring(start, end);
210+
}
211+
},
212+
_a.DOUBLE_QUOTE = '"',
213+
_a.IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
214+
_a);

0 commit comments

Comments
 (0)