mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-11 18:41:06 -06:00
For #269213 This adds a new eslint rule for `as any` and `<any>({... })`. We'd like to remove almost all of these, however right now the first goal is to prevent them in new code. That's why with this first PR I simply add `eslint-disable` comments for all breaks Trying to get this change in soon after branching off for release to hopefully minimize disruption during debt week work
163 lines
6.0 KiB
TypeScript
163 lines
6.0 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 * as eslint from 'eslint';
|
|
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/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: 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,
|
|
};
|
|
|
|
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);
|
|
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',
|
|
// eslint-disable-next-line local/code-no-any-casts
|
|
data: { message: context.getSourceCode().getText(<any>node) }
|
|
});
|
|
}
|
|
}
|
|
|
|
function visitL10NCall(node: TSESTree.CallExpression) {
|
|
|
|
// localize(key, message)
|
|
const [messageNode] = (<TSESTree.CallExpression>node).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' });
|
|
}
|
|
|
|
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++) {
|
|
// eslint-disable-next-line local/code-no-any-casts
|
|
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),
|
|
['ExpressionStatement[directive] Literal:exit']: (node: any) => doubleQuotedStringLiterals.delete(node),
|
|
|
|
// localize(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node),
|
|
|
|
// localize2(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize2"]:exit']: (node: any) => visitLocalizeCall(node),
|
|
|
|
// vscode.l10n.t(...)
|
|
['CallExpression[callee.type="MemberExpression"][callee.object.property.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),
|
|
|
|
// l10n.t(...)
|
|
['CallExpression[callee.object.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),
|
|
|
|
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
|
['CallExpression[callee.name="localize2"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
|
['Program:exit']: reportBadStringsAndBadKeys,
|
|
};
|
|
}
|
|
};
|