UNPKG

eslint-plugin-sf-plugin

Version:
206 lines (205 loc) 10.5 kB
"use strict"; 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); };