mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-10 00:27:05 -06:00
Wth node 20.18, we can now run these typescript files directly instead of having to use ts-node
193 lines
7.3 KiB
TypeScript
193 lines
7.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
|
import * as eslint from 'eslint';
|
|
import type * as ESTree from 'estree';
|
|
|
|
function isStringLiteral(node: TSESTree.Node | ESTree.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] === '"';
|
|
}
|
|
|
|
/**
|
|
* Enable bulk fixing double-quoted strings to single-quoted strings with the --fix eslint flag
|
|
*
|
|
* Disabled by default as this is often not the desired fix. Instead the string should be localized. However it is
|
|
* useful for bulk conversations of existing code.
|
|
*/
|
|
const enableDoubleToSingleQuoteFixes = false;
|
|
|
|
|
|
export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
|
|
|
|
private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/;
|
|
|
|
readonly meta: eslint.Rule.RuleMetaData = {
|
|
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.'
|
|
},
|
|
schema: false,
|
|
fixable: enableDoubleToSingleQuoteFixes ? 'code' : undefined,
|
|
};
|
|
|
|
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: ESTree.Literal) {
|
|
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
|
doubleQuotedStringLiterals.add(node);
|
|
}
|
|
}
|
|
|
|
function visitLocalizeCall(node: TSESTree.CallExpression) {
|
|
|
|
// localize(key, message)
|
|
const [keyNode, messageNode] = node.arguments;
|
|
|
|
// (1)
|
|
// extract key so that it can be checked later
|
|
let key: string | undefined;
|
|
if (isStringLiteral(keyNode)) {
|
|
doubleQuotedStringLiterals.delete(keyNode);
|
|
key = keyNode.value;
|
|
|
|
} else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) {
|
|
for (const 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);
|
|
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 as ESTree.Node) }
|
|
});
|
|
}
|
|
}
|
|
|
|
function visitL10NCall(node: TSESTree.CallExpression) {
|
|
|
|
// localize(key, message)
|
|
const [messageNode] = (node as TSESTree.CallExpression).arguments; // remove message-argument from doubleQuoted list and make
|
|
// sure it is a string-literal
|
|
if (isStringLiteral(messageNode)) {
|
|
doubleQuotedStringLiterals.delete(messageNode);
|
|
} else if (messageNode.type === AST_NODE_TYPES.ObjectExpression) {
|
|
for (const prop of messageNode.properties) {
|
|
if (prop.type === AST_NODE_TYPES.Property) {
|
|
if (prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'message') {
|
|
doubleQuotedStringLiterals.delete(prop.value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function reportBadStringsAndBadKeys() {
|
|
// (1)
|
|
// report all strings that are in double quotes
|
|
for (const node of doubleQuotedStringLiterals) {
|
|
context.report({
|
|
loc: node.loc,
|
|
messageId: 'doubleQuoted',
|
|
fix: enableDoubleToSingleQuoteFixes ? (fixer) => {
|
|
// Get the raw string content, unescaping any escaped quotes
|
|
const content = (node as ESTree.SimpleLiteral).raw!
|
|
.slice(1, -1)
|
|
.replace(/(?<!\\)\\'/g, `'`)
|
|
.replace(/(?<!\\)\\"/g, `"`);
|
|
|
|
// If the escaped content contains a single quote, use template string instead
|
|
if (content.includes(`'`)
|
|
&& !content.includes('${') // Unless the content has a template expressions
|
|
&& !content.includes('`') // Or backticks which would need escaping
|
|
) {
|
|
const templateStr = `\`${content}\``;
|
|
return fixer.replaceText(node, templateStr);
|
|
}
|
|
|
|
// Otherwise prefer using a single-quoted string
|
|
const singleStr = `'${content.replace(/'/g, `\\'`)}'`;
|
|
return fixer.replaceText(node, singleStr);
|
|
} : undefined
|
|
});
|
|
}
|
|
|
|
for (const [key, values] of externalizedStringLiterals) {
|
|
|
|
// (2)
|
|
// report all invalid NLS keys
|
|
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
|
for (const 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 as ESTree.Node) !== context.getSourceCode().getText(values[i].message as ESTree.Node)) {
|
|
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
['Literal']: (node: ESTree.Literal) => collectDoubleQuotedStrings(node),
|
|
['ExpressionStatement[directive] Literal:exit']: (node: TSESTree.Literal) => doubleQuotedStringLiterals.delete(node),
|
|
|
|
// localize(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node),
|
|
|
|
// localize2(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize2"]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node),
|
|
|
|
// vscode.l10n.t(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.property.name="l10n"][callee.property.name="t"]:exit']: (node: TSESTree.CallExpression) => visitL10NCall(node),
|
|
|
|
// l10n.t(...)
|
|
['CallExpression[callee.object.name="l10n"][callee.property.name="t"]:exit']: (node: TSESTree.CallExpression) => visitL10NCall(node),
|
|
|
|
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node),
|
|
['CallExpression[callee.name="localize2"][arguments.length>=2]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node),
|
|
['Program:exit']: reportBadStringsAndBadKeys,
|
|
};
|
|
}
|
|
};
|