mirror of
https://github.com/Microsoft/vscode
synced 2024-10-04 02:14:06 +00:00
only one double-qoute string rule
This commit is contained in:
parent
f885d7769a
commit
e27d7cafa1
|
@ -31,18 +31,7 @@
|
||||||
"code-translation-remind": "warn",
|
"code-translation-remind": "warn",
|
||||||
"code-no-nls-in-standalone-editor": "warn",
|
"code-no-nls-in-standalone-editor": "warn",
|
||||||
"code-no-standalone-editor": "warn",
|
"code-no-standalone-editor": "warn",
|
||||||
"code-no-unexternalized-strings2": "warn",
|
"code-no-unexternalized-strings": "warn",
|
||||||
"code-no-unexternalized-strings": [
|
|
||||||
"off",
|
|
||||||
{
|
|
||||||
"signatures": [
|
|
||||||
"localize",
|
|
||||||
"nls.localize"
|
|
||||||
],
|
|
||||||
"keyIndex": 0,
|
|
||||||
"messageIndex": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"code-layering": [
|
"code-layering": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,207 +8,105 @@ const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
|
||||||
function isStringLiteral(node) {
|
function isStringLiteral(node) {
|
||||||
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
||||||
}
|
}
|
||||||
function isObjectLiteral(node) {
|
function isDoubleQuoted(node) {
|
||||||
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression;
|
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
|
||||||
}
|
}
|
||||||
function isPropertyAssignment(node) {
|
module.exports = new (_a = class NoUnexternalizedStrings {
|
||||||
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Property;
|
|
||||||
}
|
|
||||||
module.exports = new (_a = class NoUnexternalizedStringsRuleWalker {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.signatures = Object.create(null);
|
|
||||||
this.ignores = Object.create(null);
|
|
||||||
this.usedKeys = Object.create(null);
|
|
||||||
this.meta = {
|
this.meta = {
|
||||||
type: 'problem',
|
type: 'problem',
|
||||||
schema: {},
|
schema: {},
|
||||||
messages: {
|
messages: {
|
||||||
badQuotes: 'Do not use double quotes for imports.',
|
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
|
||||||
unexternalized: 'Unexternalized string.',
|
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
|
||||||
duplicateKey: `Duplicate key '{{key}}' with different message value.`,
|
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
|
||||||
badKey: `The key {{key}} doesn't conform to a valid localize identifier`,
|
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
|
||||||
emptyKey: 'Key is empty.',
|
|
||||||
whitespaceKey: 'Key is only whitespace.',
|
|
||||||
badMessage: `Message argument to '{{message}}' must be a string literal.`
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
create(context) {
|
create(context) {
|
||||||
const first = context.options[0];
|
const externalizedStringLiterals = new Map();
|
||||||
if (first) {
|
const doubleQuotedStringLiterals = new Set();
|
||||||
if (Array.isArray(first.signatures)) {
|
function collectDoubleQuotedStrings(node) {
|
||||||
first.signatures.forEach((signature) => this.signatures[signature] = true);
|
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
||||||
}
|
doubleQuotedStringLiterals.add(node);
|
||||||
if (Array.isArray(first.ignores)) {
|
|
||||||
first.ignores.forEach((ignore) => this.ignores[ignore] = true);
|
|
||||||
}
|
|
||||||
if (typeof first.messageIndex !== 'undefined') {
|
|
||||||
this.messageIndex = first.messageIndex;
|
|
||||||
}
|
|
||||||
if (typeof first.keyIndex !== 'undefined') {
|
|
||||||
this.keyIndex = first.keyIndex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
function visitLocalizeCall(node) {
|
||||||
['Program:exit']: () => {
|
// localize(key, message)
|
||||||
this._checkProgramEnd(context);
|
const [keyNode, messageNode] = node.arguments;
|
||||||
},
|
// (1)
|
||||||
['Literal']: (node) => {
|
// extract key so that it can be checked later
|
||||||
if (typeof node.value === 'string') {
|
let key;
|
||||||
this._checkStringLiteral(context, node);
|
if (isStringLiteral(keyNode)) {
|
||||||
}
|
doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider
|
||||||
},
|
key = keyNode.value;
|
||||||
};
|
|
||||||
}
|
|
||||||
_checkProgramEnd(context) {
|
|
||||||
Object.keys(this.usedKeys).forEach(key => {
|
|
||||||
// Keys are quoted.
|
|
||||||
const identifier = key.substr(1, key.length - 2);
|
|
||||||
const occurrences = this.usedKeys[key];
|
|
||||||
// bad key
|
|
||||||
if (!NoUnexternalizedStringsRuleWalker.IDENTIFIER.test(identifier)) {
|
|
||||||
context.report({
|
|
||||||
loc: occurrences[0].key.loc,
|
|
||||||
messageId: 'badKey',
|
|
||||||
data: { key: occurrences[0].key.value }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// duplicates key
|
else if (keyNode.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression) {
|
||||||
if (occurrences.length > 1) {
|
for (let property of keyNode.properties) {
|
||||||
occurrences.forEach(occurrence => {
|
if (property.type === experimental_utils_1.AST_NODE_TYPES.Property && !property.computed) {
|
||||||
context.report({
|
if (property.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier && property.key.name === 'key') {
|
||||||
loc: occurrence.key.loc,
|
if (isStringLiteral(property.value)) {
|
||||||
messageId: 'duplicateKey',
|
doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider
|
||||||
data: { key: occurrence.key.value }
|
key = property.value.value;
|
||||||
});
|
break;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_checkStringLiteral(context, node) {
|
|
||||||
var _a;
|
|
||||||
const text = node.raw;
|
|
||||||
const doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE;
|
|
||||||
const info = this._findDescribingParent(node);
|
|
||||||
// Ignore strings in import and export nodes.
|
|
||||||
if (info && info.isImport && doubleQuoted) {
|
|
||||||
context.report({
|
|
||||||
loc: node.loc,
|
|
||||||
messageId: 'badQuotes'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callInfo = info ? info.callInfo : null;
|
|
||||||
const functionName = callInfo && isStringLiteral(callInfo.callExpression.callee)
|
|
||||||
? callInfo.callExpression.callee.value
|
|
||||||
: null;
|
|
||||||
if (functionName && this.ignores[functionName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) {
|
|
||||||
context.report({
|
|
||||||
loc: node.loc,
|
|
||||||
messageId: 'unexternalized'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We have a single quoted string outside a localize function name.
|
|
||||||
if (!doubleQuoted && !this.signatures[functionName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We have a string that is a direct argument into the localize call.
|
|
||||||
const keyArg = callInfo && callInfo.argIndex === this.keyIndex
|
|
||||||
? callInfo.callExpression.arguments[this.keyIndex]
|
|
||||||
: null;
|
|
||||||
if (keyArg) {
|
|
||||||
if (isStringLiteral(keyArg)) {
|
|
||||||
this.recordKey(context, keyArg, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
|
|
||||||
}
|
|
||||||
else if (isObjectLiteral(keyArg)) {
|
|
||||||
for (const property of keyArg.properties) {
|
|
||||||
if (isPropertyAssignment(property)) {
|
|
||||||
const name = NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), property.key);
|
|
||||||
if (name === 'key') {
|
|
||||||
const initializer = property.value;
|
|
||||||
if (isStringLiteral(initializer)) {
|
|
||||||
this.recordKey(context, initializer, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
let array = externalizedStringLiterals.get(key);
|
||||||
|
if (!array) {
|
||||||
|
array = [];
|
||||||
|
externalizedStringLiterals.set(key, array);
|
||||||
|
}
|
||||||
|
array.push({ call: node, message: messageNode });
|
||||||
|
}
|
||||||
|
// (2)
|
||||||
|
// remove message-argument from doubleQuoted list and make
|
||||||
|
// sure it is a string-literal
|
||||||
|
doubleQuotedStringLiterals.delete(messageNode);
|
||||||
|
if (!isStringLiteral(messageNode)) {
|
||||||
|
context.report({
|
||||||
|
loc: messageNode.loc,
|
||||||
|
messageId: 'badMessage',
|
||||||
|
data: { message: context.getSourceCode().getText(node) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function reportBadStringsAndBadKeys() {
|
||||||
|
// (1)
|
||||||
|
// report all strings that are in double quotes
|
||||||
|
for (const node of doubleQuotedStringLiterals) {
|
||||||
|
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
|
||||||
|
}
|
||||||
|
for (const [key, values] of externalizedStringLiterals) {
|
||||||
|
// (2)
|
||||||
|
// report all invalid NLS keys
|
||||||
|
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
||||||
|
for (let value of values) {
|
||||||
|
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// (2)
|
||||||
|
// report all invalid duplicates (same key, different message)
|
||||||
|
if (values.length > 1) {
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) {
|
||||||
|
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const messageArg = callInfo.callExpression.arguments[this.messageIndex];
|
return {
|
||||||
if (messageArg && !isStringLiteral(messageArg)) {
|
['Literal']: (node) => collectDoubleQuotedStrings(node),
|
||||||
context.report({
|
['CallExpression[callee.type="MemberExpression"][callee.property.name="localize"]:exit']: (node) => visitLocalizeCall(node),
|
||||||
loc: messageArg.loc,
|
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node) => visitLocalizeCall(node),
|
||||||
messageId: 'badMessage',
|
['Program:exit']: reportBadStringsAndBadKeys,
|
||||||
data: { message: NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), (_a = callInfo) === null || _a === void 0 ? void 0 : _a.callExpression.callee) }
|
};
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recordKey(context, keyNode, messageNode) {
|
|
||||||
const text = keyNode.raw;
|
|
||||||
// We have an empty key
|
|
||||||
if (text.match(/(['"]) *\1/)) {
|
|
||||||
if (messageNode) {
|
|
||||||
context.report({
|
|
||||||
loc: keyNode.loc,
|
|
||||||
messageId: 'whitespaceKey'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
context.report({
|
|
||||||
loc: keyNode.loc,
|
|
||||||
messageId: 'emptyKey'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let occurrences = this.usedKeys[text];
|
|
||||||
if (!occurrences) {
|
|
||||||
occurrences = [];
|
|
||||||
this.usedKeys[text] = occurrences;
|
|
||||||
}
|
|
||||||
if (messageNode) {
|
|
||||||
if (occurrences.some(pair => pair.message ? NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), pair.message) === NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), messageNode) : false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
occurrences.push({ key: keyNode, message: messageNode });
|
|
||||||
}
|
|
||||||
_findDescribingParent(node) {
|
|
||||||
let parent;
|
|
||||||
while ((parent = node.parent)) {
|
|
||||||
const kind = parent.type;
|
|
||||||
if (kind === experimental_utils_1.AST_NODE_TYPES.CallExpression) {
|
|
||||||
const callExpression = parent;
|
|
||||||
return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } };
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
return { isImport: true };
|
|
||||||
}
|
|
||||||
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
|
|
||||||
|| kind === experimental_utils_1.AST_NODE_TYPES.TSMethodSignature || kind === experimental_utils_1.AST_NODE_TYPES.TSInterfaceDeclaration
|
|
||||||
|| kind === experimental_utils_1.AST_NODE_TYPES.ClassDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.TSEnumDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.TSModuleDeclaration
|
|
||||||
|| kind === experimental_utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration || kind === experimental_utils_1.AST_NODE_TYPES.Program) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
node = parent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
static _getText(source, node) {
|
|
||||||
if (node.type === experimental_utils_1.AST_NODE_TYPES.Literal) {
|
|
||||||
return String(node.value);
|
|
||||||
}
|
|
||||||
const start = source.getIndexFromLoc(node.loc.start);
|
|
||||||
const end = source.getIndexFromLoc(node.loc.end);
|
|
||||||
return source.getText().substring(start, end);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_a.DOUBLE_QUOTE = '"',
|
_a._rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
|
||||||
_a.IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
|
|
||||||
_a);
|
_a);
|
||||||
|
|
|
@ -6,250 +6,122 @@
|
||||||
import * as eslint from 'eslint';
|
import * as eslint from 'eslint';
|
||||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
|
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
|
||||||
|
|
||||||
interface Map<V> {
|
|
||||||
[key: string]: V;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnexternalizedStringsOptions {
|
|
||||||
signatures?: string[];
|
|
||||||
messageIndex?: number;
|
|
||||||
keyIndex?: number;
|
|
||||||
ignores?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral {
|
function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral {
|
||||||
return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObjectLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.ObjectExpression {
|
function isDoubleQuoted(node: TSESTree.StringLiteral): boolean {
|
||||||
return !!node && node.type === AST_NODE_TYPES.ObjectExpression;
|
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPropertyAssignment(node: TSESTree.Node | null | undefined): node is TSESTree.Property {
|
export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
|
||||||
return !!node && node.type === AST_NODE_TYPES.Property;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeyMessagePair {
|
private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/;
|
||||||
key: TSESTree.StringLiteral;
|
|
||||||
message: TSESTree.Node | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export = new class NoUnexternalizedStringsRuleWalker implements eslint.Rule.RuleModule {
|
|
||||||
|
|
||||||
private static DOUBLE_QUOTE: string = '"';
|
|
||||||
private static IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/;
|
|
||||||
|
|
||||||
private signatures: Map<boolean> = Object.create(null);
|
|
||||||
private messageIndex: number | undefined;
|
|
||||||
private keyIndex: number | undefined;
|
|
||||||
private ignores: Map<boolean> = Object.create(null);
|
|
||||||
|
|
||||||
private usedKeys: Map<KeyMessagePair[]> = Object.create(null);
|
|
||||||
|
|
||||||
readonly meta = {
|
readonly meta = {
|
||||||
type: 'problem',
|
type: 'problem',
|
||||||
schema: {},
|
schema: {},
|
||||||
messages: {
|
messages: {
|
||||||
badQuotes: 'Do not use double quotes for imports.',
|
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
|
||||||
unexternalized: 'Unexternalized string.',
|
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
|
||||||
duplicateKey: `Duplicate key '{{key}}' with different message value.`,
|
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
|
||||||
badKey: `The key {{key}} doesn't conform to a valid localize identifier`,
|
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
|
||||||
emptyKey: 'Key is empty.',
|
|
||||||
whitespaceKey: 'Key is only whitespace.',
|
|
||||||
badMessage: `Message argument to '{{message}}' must be a string literal.`
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||||
|
|
||||||
const first = <UnexternalizedStringsOptions>context.options[0];
|
const externalizedStringLiterals = new Map<string, { call: TSESTree.CallExpression, message: TSESTree.Node }[]>();
|
||||||
if (first) {
|
const doubleQuotedStringLiterals = new Set<TSESTree.Node>();
|
||||||
if (Array.isArray(first.signatures)) {
|
|
||||||
first.signatures.forEach((signature: string) => this.signatures[signature] = true);
|
function collectDoubleQuotedStrings(node: TSESTree.Literal) {
|
||||||
}
|
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
||||||
if (Array.isArray(first.ignores)) {
|
doubleQuotedStringLiterals.add(node);
|
||||||
first.ignores.forEach((ignore: string) => this.ignores[ignore] = true);
|
|
||||||
}
|
|
||||||
if (typeof first.messageIndex !== 'undefined') {
|
|
||||||
this.messageIndex = first.messageIndex;
|
|
||||||
}
|
|
||||||
if (typeof first.keyIndex !== 'undefined') {
|
|
||||||
this.keyIndex = first.keyIndex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
function visitLocalizeCall(node: TSESTree.CallExpression) {
|
||||||
['Program:exit']: () => {
|
|
||||||
this._checkProgramEnd(context);
|
|
||||||
},
|
|
||||||
['Literal']: (node: any) => {
|
|
||||||
if (typeof (<TSESTree.Literal>node).value === 'string') {
|
|
||||||
this._checkStringLiteral(context, node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _checkProgramEnd(context: eslint.Rule.RuleContext): void {
|
// localize(key, message)
|
||||||
Object.keys(this.usedKeys).forEach(key => {
|
const [keyNode, messageNode] = (<TSESTree.CallExpression>node).arguments;
|
||||||
// Keys are quoted.
|
|
||||||
const identifier = key.substr(1, key.length - 2);
|
|
||||||
const occurrences = this.usedKeys[key];
|
|
||||||
|
|
||||||
// bad key
|
// (1)
|
||||||
if (!NoUnexternalizedStringsRuleWalker.IDENTIFIER.test(identifier)) {
|
// extract key so that it can be checked later
|
||||||
context.report({
|
let key: string | undefined;
|
||||||
loc: occurrences[0].key.loc,
|
if (isStringLiteral(keyNode)) {
|
||||||
messageId: 'badKey',
|
doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider
|
||||||
data: { key: occurrences[0].key.value }
|
key = keyNode.value;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// duplicates key
|
} else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) {
|
||||||
if (occurrences.length > 1) {
|
for (let property of keyNode.properties) {
|
||||||
occurrences.forEach(occurrence => {
|
if (property.type === AST_NODE_TYPES.Property && !property.computed) {
|
||||||
context.report({
|
if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') {
|
||||||
loc: occurrence.key.loc,
|
if (isStringLiteral(property.value)) {
|
||||||
messageId: 'duplicateKey',
|
doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider
|
||||||
data: { key: occurrence.key.value }
|
key = property.value.value;
|
||||||
});
|
break;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkStringLiteral(context: eslint.Rule.RuleContext, node: TSESTree.StringLiteral): void {
|
|
||||||
const text = node.raw;
|
|
||||||
const doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE;
|
|
||||||
const info = this._findDescribingParent(node);
|
|
||||||
|
|
||||||
// Ignore strings in import and export nodes.
|
|
||||||
if (info && info.isImport && doubleQuoted) {
|
|
||||||
context.report({
|
|
||||||
loc: node.loc,
|
|
||||||
messageId: 'badQuotes'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const callInfo = info ? info.callInfo : null;
|
|
||||||
const functionName = callInfo && isStringLiteral(callInfo.callExpression.callee)
|
|
||||||
? callInfo.callExpression.callee.value
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (functionName && this.ignores[functionName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName!])) {
|
|
||||||
context.report({
|
|
||||||
loc: node.loc,
|
|
||||||
messageId: 'unexternalized'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a single quoted string outside a localize function name.
|
|
||||||
if (!doubleQuoted && !this.signatures[functionName!]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We have a string that is a direct argument into the localize call.
|
|
||||||
const keyArg: TSESTree.Expression | null = callInfo && callInfo.argIndex === this.keyIndex
|
|
||||||
? callInfo.callExpression.arguments[this.keyIndex]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (keyArg) {
|
|
||||||
if (isStringLiteral(keyArg)) {
|
|
||||||
this.recordKey(context, keyArg, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
|
|
||||||
|
|
||||||
} else if (isObjectLiteral(keyArg)) {
|
|
||||||
for (const property of keyArg.properties) {
|
|
||||||
if (isPropertyAssignment(property)) {
|
|
||||||
const name = NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), property.key);
|
|
||||||
if (name === 'key') {
|
|
||||||
const initializer = property.value;
|
|
||||||
if (isStringLiteral(initializer)) {
|
|
||||||
this.recordKey(context, initializer, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined);
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
let array = externalizedStringLiterals.get(key);
|
||||||
|
if (!array) {
|
||||||
|
array = [];
|
||||||
|
externalizedStringLiterals.set(key, array);
|
||||||
|
}
|
||||||
|
array.push({ call: node, message: messageNode });
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2)
|
||||||
|
// remove message-argument from doubleQuoted list and make
|
||||||
|
// sure it is a string-literal
|
||||||
|
doubleQuotedStringLiterals.delete(messageNode);
|
||||||
|
if (!isStringLiteral(messageNode)) {
|
||||||
|
context.report({
|
||||||
|
loc: messageNode.loc,
|
||||||
|
messageId: 'badMessage',
|
||||||
|
data: { message: context.getSourceCode().getText(<any>node) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportBadStringsAndBadKeys() {
|
||||||
|
// (1)
|
||||||
|
// report all strings that are in double quotes
|
||||||
|
for (const node of doubleQuotedStringLiterals) {
|
||||||
|
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, values] of externalizedStringLiterals) {
|
||||||
|
|
||||||
|
// (2)
|
||||||
|
// report all invalid NLS keys
|
||||||
|
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
||||||
|
for (let value of values) {
|
||||||
|
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2)
|
||||||
|
// report all invalid duplicates (same key, different message)
|
||||||
|
if (values.length > 1) {
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
if (context.getSourceCode().getText(<any>values[i - 1].message) !== context.getSourceCode().getText(<any>values[i].message)) {
|
||||||
|
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageArg = callInfo!.callExpression.arguments[this.messageIndex!];
|
return {
|
||||||
|
['Literal']: (node: any) => collectDoubleQuotedStrings(node),
|
||||||
if (messageArg && !isStringLiteral(messageArg)) {
|
['CallExpression[callee.type="MemberExpression"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node),
|
||||||
context.report({
|
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
||||||
loc: messageArg.loc,
|
['Program:exit']: reportBadStringsAndBadKeys,
|
||||||
messageId: 'badMessage',
|
};
|
||||||
data: { message: NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), callInfo?.callExpression.callee!) }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private recordKey(context: eslint.Rule.RuleContext, keyNode: TSESTree.StringLiteral, messageNode: TSESTree.Node | undefined) {
|
|
||||||
const text = keyNode.raw;
|
|
||||||
// We have an empty key
|
|
||||||
if (text.match(/(['"]) *\1/)) {
|
|
||||||
if (messageNode) {
|
|
||||||
context.report({
|
|
||||||
loc: keyNode.loc,
|
|
||||||
messageId: 'whitespaceKey'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
context.report({
|
|
||||||
loc: keyNode.loc,
|
|
||||||
messageId: 'emptyKey'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let occurrences: KeyMessagePair[] = this.usedKeys[text];
|
|
||||||
if (!occurrences) {
|
|
||||||
occurrences = [];
|
|
||||||
this.usedKeys[text] = occurrences;
|
|
||||||
}
|
|
||||||
if (messageNode) {
|
|
||||||
if (occurrences.some(pair => pair.message ? NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), pair.message) === NoUnexternalizedStringsRuleWalker._getText(context.getSourceCode(), messageNode) : false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
occurrences.push({ key: keyNode, message: messageNode });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _findDescribingParent(node: TSESTree.Node): { callInfo?: { callExpression: TSESTree.CallExpression, argIndex: number }, isImport?: boolean; } | null {
|
|
||||||
let parent: TSESTree.Node | undefined;
|
|
||||||
while ((parent = node.parent)) {
|
|
||||||
const kind = parent.type;
|
|
||||||
if (kind === AST_NODE_TYPES.CallExpression) {
|
|
||||||
const callExpression = parent as TSESTree.CallExpression;
|
|
||||||
return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(<any>node) } };
|
|
||||||
|
|
||||||
} else if (kind === AST_NODE_TYPES.TSImportEqualsDeclaration || kind === AST_NODE_TYPES.ImportDeclaration || kind === AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
||||||
return { isImport: true };
|
|
||||||
|
|
||||||
} else if (kind === AST_NODE_TYPES.VariableDeclaration || kind === AST_NODE_TYPES.FunctionDeclaration || kind === AST_NODE_TYPES.TSPropertySignature
|
|
||||||
|| kind === AST_NODE_TYPES.TSMethodSignature || kind === AST_NODE_TYPES.TSInterfaceDeclaration
|
|
||||||
|| kind === AST_NODE_TYPES.ClassDeclaration || kind === AST_NODE_TYPES.TSEnumDeclaration || kind === AST_NODE_TYPES.TSModuleDeclaration
|
|
||||||
|| kind === AST_NODE_TYPES.TSTypeAliasDeclaration || kind === AST_NODE_TYPES.Program) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
node = parent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static _getText(source: eslint.SourceCode, node: TSESTree.Node): string {
|
|
||||||
if (node.type === AST_NODE_TYPES.Literal) {
|
|
||||||
return String(node.value);
|
|
||||||
}
|
|
||||||
const start = source.getIndexFromLoc(node.loc.start);
|
|
||||||
const end = source.getIndexFromLoc(node.loc.end);
|
|
||||||
return source.getText().substring(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
"use strict";
|
|
||||||
/*---------------------------------------------------------------------------------------------
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
||||||
*--------------------------------------------------------------------------------------------*/
|
|
||||||
var _a;
|
|
||||||
const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
|
|
||||||
function isStringLiteral(node) {
|
|
||||||
return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
|
||||||
}
|
|
||||||
function isDoubleQuoted(node) {
|
|
||||||
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
|
|
||||||
}
|
|
||||||
module.exports = new (_a = class NoUnexternalizedStrings {
|
|
||||||
constructor() {
|
|
||||||
this.meta = {
|
|
||||||
type: 'problem',
|
|
||||||
schema: {},
|
|
||||||
messages: {
|
|
||||||
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
|
|
||||||
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
|
|
||||||
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
|
|
||||||
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
create(context) {
|
|
||||||
const externalizedStringLiterals = new Map();
|
|
||||||
const doubleQuotedStringLiterals = new Set();
|
|
||||||
function collectDoubleQuotedStrings(node) {
|
|
||||||
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
|
||||||
doubleQuotedStringLiterals.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function visitLocalizeCall(node) {
|
|
||||||
// localize(key, message)
|
|
||||||
const [keyNode, messageNode] = node.arguments;
|
|
||||||
// (1)
|
|
||||||
// extract key so that it can be checked later
|
|
||||||
let key;
|
|
||||||
if (isStringLiteral(keyNode)) {
|
|
||||||
doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider
|
|
||||||
key = keyNode.value;
|
|
||||||
}
|
|
||||||
else if (keyNode.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
||||||
for (let property of keyNode.properties) {
|
|
||||||
if (property.type === experimental_utils_1.AST_NODE_TYPES.Property && !property.computed) {
|
|
||||||
if (property.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier && property.key.name === 'key') {
|
|
||||||
if (isStringLiteral(property.value)) {
|
|
||||||
doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider
|
|
||||||
key = property.value.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof key === 'string') {
|
|
||||||
let array = externalizedStringLiterals.get(key);
|
|
||||||
if (!array) {
|
|
||||||
array = [];
|
|
||||||
externalizedStringLiterals.set(key, array);
|
|
||||||
}
|
|
||||||
array.push({ call: node, message: messageNode });
|
|
||||||
}
|
|
||||||
// (2)
|
|
||||||
// remove message-argument from doubleQuoted list and make
|
|
||||||
// sure it is a string-literal
|
|
||||||
doubleQuotedStringLiterals.delete(messageNode);
|
|
||||||
if (!isStringLiteral(messageNode)) {
|
|
||||||
context.report({
|
|
||||||
loc: messageNode.loc,
|
|
||||||
messageId: 'badMessage',
|
|
||||||
data: { message: context.getSourceCode().getText(node) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function reportBadStringsAndBadKeys() {
|
|
||||||
// (1)
|
|
||||||
// report all strings that are in double quotes
|
|
||||||
for (const node of doubleQuotedStringLiterals) {
|
|
||||||
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
|
|
||||||
}
|
|
||||||
for (const [key, values] of externalizedStringLiterals) {
|
|
||||||
// (2)
|
|
||||||
// report all invalid NLS keys
|
|
||||||
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
|
||||||
for (let value of values) {
|
|
||||||
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// (2)
|
|
||||||
// report all invalid duplicates (same key, different message)
|
|
||||||
if (values.length > 1) {
|
|
||||||
for (let i = 1; i < values.length; i++) {
|
|
||||||
if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) {
|
|
||||||
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
['Literal']: (node) => collectDoubleQuotedStrings(node),
|
|
||||||
['CallExpression[callee.type="MemberExpression"][callee.property.name="localize"]:exit']: (node) => visitLocalizeCall(node),
|
|
||||||
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node) => visitLocalizeCall(node),
|
|
||||||
['Program:exit']: reportBadStringsAndBadKeys,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_a._rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/,
|
|
||||||
_a);
|
|
|
@ -1,127 +0,0 @@
|
||||||
/*---------------------------------------------------------------------------------------------
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
||||||
*--------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
import * as eslint from 'eslint';
|
|
||||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
|
|
||||||
|
|
||||||
function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral {
|
|
||||||
return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDoubleQuoted(node: TSESTree.StringLiteral): boolean {
|
|
||||||
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
|
|
||||||
|
|
||||||
private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/;
|
|
||||||
|
|
||||||
readonly meta = {
|
|
||||||
type: 'problem',
|
|
||||||
schema: {},
|
|
||||||
messages: {
|
|
||||||
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
|
|
||||||
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
|
|
||||||
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
|
|
||||||
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
|
||||||
|
|
||||||
const externalizedStringLiterals = new Map<string, { call: TSESTree.CallExpression, message: TSESTree.Node }[]>();
|
|
||||||
const doubleQuotedStringLiterals = new Set<TSESTree.Node>();
|
|
||||||
|
|
||||||
function collectDoubleQuotedStrings(node: TSESTree.Literal) {
|
|
||||||
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
|
||||||
doubleQuotedStringLiterals.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function visitLocalizeCall(node: TSESTree.CallExpression) {
|
|
||||||
|
|
||||||
// localize(key, message)
|
|
||||||
const [keyNode, messageNode] = (<TSESTree.CallExpression>node).arguments;
|
|
||||||
|
|
||||||
// (1)
|
|
||||||
// extract key so that it can be checked later
|
|
||||||
let key: string | undefined;
|
|
||||||
if (isStringLiteral(keyNode)) {
|
|
||||||
doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider
|
|
||||||
key = keyNode.value;
|
|
||||||
|
|
||||||
} else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) {
|
|
||||||
for (let property of keyNode.properties) {
|
|
||||||
if (property.type === AST_NODE_TYPES.Property && !property.computed) {
|
|
||||||
if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') {
|
|
||||||
if (isStringLiteral(property.value)) {
|
|
||||||
doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider
|
|
||||||
key = property.value.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof key === 'string') {
|
|
||||||
let array = externalizedStringLiterals.get(key);
|
|
||||||
if (!array) {
|
|
||||||
array = [];
|
|
||||||
externalizedStringLiterals.set(key, array);
|
|
||||||
}
|
|
||||||
array.push({ call: node, message: messageNode });
|
|
||||||
}
|
|
||||||
|
|
||||||
// (2)
|
|
||||||
// remove message-argument from doubleQuoted list and make
|
|
||||||
// sure it is a string-literal
|
|
||||||
doubleQuotedStringLiterals.delete(messageNode);
|
|
||||||
if (!isStringLiteral(messageNode)) {
|
|
||||||
context.report({
|
|
||||||
loc: messageNode.loc,
|
|
||||||
messageId: 'badMessage',
|
|
||||||
data: { message: context.getSourceCode().getText(<any>node) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportBadStringsAndBadKeys() {
|
|
||||||
// (1)
|
|
||||||
// report all strings that are in double quotes
|
|
||||||
for (const node of doubleQuotedStringLiterals) {
|
|
||||||
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, values] of externalizedStringLiterals) {
|
|
||||||
|
|
||||||
// (2)
|
|
||||||
// report all invalid NLS keys
|
|
||||||
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
|
||||||
for (let value of values) {
|
|
||||||
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (2)
|
|
||||||
// report all invalid duplicates (same key, different message)
|
|
||||||
if (values.length > 1) {
|
|
||||||
for (let i = 1; i < values.length; i++) {
|
|
||||||
if (context.getSourceCode().getText(<any>values[i - 1].message) !== context.getSourceCode().getText(<any>values[i].message)) {
|
|
||||||
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
['Literal']: (node: any) => collectDoubleQuotedStrings(node),
|
|
||||||
['CallExpression[callee.type="MemberExpression"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node),
|
|
||||||
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
|
||||||
['Program:exit']: reportBadStringsAndBadKeys,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
Loading…
Reference in a new issue