eslint-plugin-sf-plugin
Version:
Helpful eslint rules for sf plugins.
206 lines (205 loc) • 10.5 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.noMissingMessages = void 0;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable complexity */
const eslint_utils_1 = require("@typescript-eslint/utils/eslint-utils");
const utils_1 = require("@typescript-eslint/utils");
const core_1 = require("@salesforce/core");
const ts = __importStar(require("typescript"));
const methods = ['createError', 'createWarning', 'createInfo', 'getMessage', 'getMessages'];
exports.noMissingMessages = eslint_utils_1.RuleCreator.withoutDocs({
meta: {
docs: {
description: 'Checks core Messages usage for correct usage of named messages and message tokens',
recommended: 'recommended',
},
messages: {
missing: 'the message "{{messageKey}}" does not exist in the messages file {{fileKey}}',
placeholders: 'the message "{{messageKey}}" in the messages file {{fileKey}} expects {{placeholderCount}} token(s) but received {{argumentCount}}',
actionPlaceholders: 'the actions for message "{{messageKey}}" in the messages file {{fileKey}} expects {{placeholderCount}} tokens(s) but received {{argumentCount}}',
},
type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
core_1.Messages.importMessagesDirectory(process.cwd());
const loadedMessages = new Map();
const loadedMessageBundles = new Map();
const parserServices = utils_1.ESLintUtils.getParserServices(context);
return {
// load any messages, by const name, that are loaded in the file
VariableDeclarator(node) {
if (node.init &&
node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
node.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
node.init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
node.init.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
node.init.callee.object.name === 'Messages' &&
node.init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
node.init.callee.property.name.startsWith('load') &&
node.init.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
typeof node.init.arguments[0].value === 'string' &&
node.init.arguments[1].type === utils_1.AST_NODE_TYPES.Literal &&
typeof node.init.arguments[1].value === 'string') {
loadedMessages.set(node.id.name, core_1.Messages.loadMessages(node.init.arguments[0].value, node.init.arguments[1].value));
loadedMessageBundles.set(node.id.name, node.init.arguments[1].value);
}
},
CallExpression(node) {
var _a, _b, _c, _d;
if (
// we don't both if we never loaded any messages
loadedMessages.size &&
node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
loadedMessages.has(node.callee.object.name) &&
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
// the key needs to be a string so we can look it up
node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
typeof node.arguments[0].value === 'string' &&
isMessagesMethod(node.callee.property.name)) {
const bundleConstant = node.callee.object.name;
const messageKey = node.arguments[0].value;
const fileKey = loadedMessageBundles.get(bundleConstant);
const messageTokensCount = getTokensCount(parserServices, node.arguments[1]);
const actionTokensCount = getTokensCount(parserServices, node.arguments[2]);
let result;
try {
// execute some method on Messages so we can inspect the result
// we are intentionally passing it no tokens so that we can see residual %s etc in the text
result = (_a = loadedMessages.get(bundleConstant)) === null || _a === void 0 ? void 0 : _a[node.callee.property.name](messageKey);
}
catch (e) {
// we never found the message at all, we can report and exit
return context.report({
node: node.arguments[0],
messageId: 'missing',
data: {
messageKey,
fileKey,
},
});
}
if (!result) {
return;
}
const resolvedMessage = getMessage(result);
if (!resolvedMessage) {
return;
}
const messagePlaceholderCount = getPlaceholderCount(resolvedMessage);
if (typeof messageTokensCount === 'number' && messagePlaceholderCount !== messageTokensCount) {
context.report({
// if there's not a second argument, we can report on the first
node: (_b = node.arguments[1]) !== null && _b !== void 0 ? _b : node.arguments[0],
messageId: 'placeholders',
data: {
placeholderCount: messagePlaceholderCount,
argumentCount: messageTokensCount,
fileKey,
messageKey,
},
});
}
// it's an SfError or a StructuredMessage, check the actions
if (typeof actionTokensCount === 'number' && typeof result !== 'string' && !Array.isArray(result)) {
const actionPlaceholderCount = getPlaceholderCount((_c = result.actions) !== null && _c !== void 0 ? _c : []);
if (actionPlaceholderCount !== actionTokensCount) {
context.report({
node: (_d = node.arguments[2]) !== null && _d !== void 0 ? _d : node.arguments[0],
messageId: 'actionPlaceholders',
data: {
placeholderCount: actionPlaceholderCount,
argumentCount: actionTokensCount,
fileKey,
messageKey,
},
});
}
}
}
},
};
},
});
// util.format placeholders https://nodejs.org/api/util.html#utilformatformat-args
const placeHolderersRegex = new RegExp(/(%s)|(%d)|(%i)|(%f)|(%j)|(%o)|(%O)|(%c)/g);
const isMessagesMethod = (method) => methods.includes(method);
const getTokensCount = (parserServices, node) => {
var _a, _b, _c;
if (!node) {
return 0;
}
if (node.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
return (_a = node.elements.length) !== null && _a !== void 0 ? _a : 0;
}
const realNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const checker = parserServices.program.getTypeChecker();
const underlyingNode = (_c = (_b = checker.getSymbolAtLocation(realNode)) === null || _b === void 0 ? void 0 : _b.getDeclarations()) === null || _c === void 0 ? void 0 : _c[0];
// the literal value might not be an array, but it might a reference to an array
if (underlyingNode &&
ts.isVariableDeclaration(underlyingNode) &&
underlyingNode.initializer &&
ts.isArrayLiteralExpression(underlyingNode.initializer)) {
return underlyingNode.initializer.elements.length;
}
return;
};
const getMessage = (result) => {
if (typeof result === 'string') {
return result;
}
if (Array.isArray(result)) {
return result;
}
if ('message' in result) {
return result.message;
}
};
const getPlaceholderCount = (message) => {
var _a;
if (typeof message === 'string') {
return ((_a = message.match(placeHolderersRegex)) !== null && _a !== void 0 ? _a : []).length;
}
return message.reduce((count, m) => { var _a; return count + ((_a = m.match(placeHolderersRegex)) !== null && _a !== void 0 ? _a : []).length; }, 0);
};
;