Skip to content

Commit e27d7ca

Browse files
committed
only one double-qoute string rule
1 parent f885d77 commit e27d7ca

5 files changed

Lines changed: 154 additions & 634 deletions

.eslintrc.json

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,7 @@
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-
],
34+
"code-no-unexternalized-strings": "warn",
4635
"code-layering": [
4736
"warn",
4837
{

build/lib/eslint/code-no-unexternalized-strings.js

Lines changed: 73 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -8,207 +8,105 @@ const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
88
function isStringLiteral(node) {
99
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string';
1010
}
11-
function isObjectLiteral(node) {
12-
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression;
11+
function isDoubleQuoted(node) {
12+
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
1313
}
14-
function isPropertyAssignment(node) {
15-
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Property;
16-
}
17-
module.exports = new (_a = class NoUnexternalizedStringsRuleWalker {
14+
module.exports = new (_a = class NoUnexternalizedStrings {
1815
constructor() {
19-
this.signatures = Object.create(null);
20-
this.ignores = Object.create(null);
21-
this.usedKeys = Object.create(null);
2216
this.meta = {
2317
type: 'problem',
2418
schema: {},
2519
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.`
20+
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
21+
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
22+
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
23+
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
3324
}
3425
};
3526
}
3627
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;
28+
const externalizedStringLiterals = new Map();
29+
const doubleQuotedStringLiterals = new Set();
30+
function collectDoubleQuotedStrings(node) {
31+
if (isStringLiteral(node) && isDoubleQuoted(node)) {
32+
doubleQuotedStringLiterals.add(node);
5033
}
5134
}
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-
});
35+
function visitLocalizeCall(node) {
36+
// localize(key, message)
37+
const [keyNode, messageNode] = node.arguments;
38+
// (1)
39+
// extract key so that it can be checked later
40+
let key;
41+
if (isStringLiteral(keyNode)) {
42+
doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider
43+
key = keyNode.value;
7544
}
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);
45+
else if (keyNode.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression) {
46+
for (let property of keyNode.properties) {
47+
if (property.type === experimental_utils_1.AST_NODE_TYPES.Property && !property.computed) {
48+
if (property.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier && property.key.name === 'key') {
49+
if (isStringLiteral(property.value)) {
50+
doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider
51+
key = property.value.value;
52+
break;
13553
}
136-
break;
13754
}
13855
}
13956
}
14057
}
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-
});
58+
if (typeof key === 'string') {
59+
let array = externalizedStringLiterals.get(key);
60+
if (!array) {
61+
array = [];
62+
externalizedStringLiterals.set(key, array);
63+
}
64+
array.push({ call: node, message: messageNode });
16165
}
162-
else {
66+
// (2)
67+
// remove message-argument from doubleQuoted list and make
68+
// sure it is a string-literal
69+
doubleQuotedStringLiterals.delete(messageNode);
70+
if (!isStringLiteral(messageNode)) {
16371
context.report({
164-
loc: keyNode.loc,
165-
messageId: 'emptyKey'
72+
loc: messageNode.loc,
73+
messageId: 'badMessage',
74+
data: { message: context.getSourceCode().getText(node) }
16675
});
16776
}
168-
return;
16977
}
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 };
78+
function reportBadStringsAndBadKeys() {
79+
// (1)
80+
// report all strings that are in double quotes
81+
for (const node of doubleQuotedStringLiterals) {
82+
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
19283
}
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;
84+
for (const [key, values] of externalizedStringLiterals) {
85+
// (2)
86+
// report all invalid NLS keys
87+
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
88+
for (let value of values) {
89+
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
90+
}
91+
}
92+
// (2)
93+
// report all invalid duplicates (same key, different message)
94+
if (values.length > 1) {
95+
for (let i = 1; i < values.length; i++) {
96+
if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) {
97+
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
98+
}
99+
}
100+
}
198101
}
199-
node = parent;
200102
}
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);
103+
return {
104+
['Literal']: (node) => collectDoubleQuotedStrings(node),
105+
['CallExpression[callee.type="MemberExpression"][callee.property.name="localize"]:exit']: (node) => visitLocalizeCall(node),
106+
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node) => visitLocalizeCall(node),
107+
['Program:exit']: reportBadStringsAndBadKeys,
108+
};
210109
}
211110
},
212-
_a.DOUBLE_QUOTE = '"',
213-
_a.IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
111+
_a._rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
214112
_a);

0 commit comments

Comments
 (0)