|
| 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