UNPKG

@admc.com/eslint-plugin-sn

Version:
97 lines (89 loc) 4.17 kB
"use strict"; /** * IF an IIFE exists, we need to check that its params are good, but there is no reason to * explicitly check for existence of IIFE since ultimate goal is to prevent root-level assignments * and declarations. * * Since this is executed by a direct ESLint script, we have no control over logging redirection. * We don't want debug messages mixing in with stdout, so log at level warning for debugging. */ const containsAll = (a1, a2) => a2.every(el => a1.includes(el)); /** * @returns boolean if ancestry has a FunctionDeclaration before any FunctionExpression */ const hasFnAncestor = ctx => ctx.getAncestors().some(node => ["FunctionExpression", "FunctionDeclaration", "ArrowFunctionExpression"].includes(node.type) ); const message = // eslint-disable-next-line max-len "For {{table}} scriptlets you must implement top-level IIFE passing at least param(s) '{{paramCallVars}}'"; const messageId = // eslint-disable-next-line prefer-template (require("path").basename(__filename).replace(/[.]js$/, "") + "_msg").toUpperCase(); const esLintObj = { meta: { type: "problem", docs: { description: "Many ServiceNow scriptlet types need an IIFE to protect from variable scope leaks", category: "Possible Problems", }, schema: [{ type: "object", properties: { table: { type: "string", }, paramCallVars: { /* IMPORTANT! If set paramCallVars then these variables must be accessible to * pass to the function. Since table-specific this would normally be done * through snglobals "tableSpecifics.json" list. * For end user customization, just add globals items to the sneslintrc.json. */ type: "array", items: { type: "string" }, uniqueItems: true }, }, additionalProperties: false }], messages: { }, }, create: context => { let iifeCount = 0; let assignAndDeclCount = 0; let goodParams = false; const reqParams = context.options[0].paramCallVars; return { CallExpression: node => { const callee = node.callee; if (!["FunctionExpression", "ArrowFunctionExpression"].includes(callee.type)) return; const rtParams = node.arguments.map(p=>p.name); //console.debug(context.getSourceCode().getText(node.callee.body)); if (node.parent.parent.type !== "Program") return; // not at block level 0/root if (context.getScope().type !== "global") return; // inside an internal function //console.debug("actual", rtParams, "vs.", //["p1", "p2"], "=", containsAll([rtParams, "p1","p2"], false)); iifeCount++; if (containsAll(rtParams, reqParams)) goodParams = true; }, AssignmentExpression: () => { if (!hasFnAncestor(context)) assignAndDeclCount++; }, VariableDeclarator: () => { if (!hasFnAncestor(context)) assignAndDeclCount++; }, FunctionDeclaration: () => { if (!hasFnAncestor(context)) assignAndDeclCount++; }, onCodePathEnd: (_dummy, node) => { if (node.type !== "Program") return; console.warn('IIFE check counts. ' + `assg ${assignAndDeclCount}, iife ${iifeCount}, goodPs ${goodParams}`); if (assignAndDeclCount === 0 && iifeCount === 0) return; // No IIFE ok if (assignAndDeclCount > 0 || !goodParams) context.report({node, messageId, data: { table: context.options[0].table, paramCallVars: context.options[0].paramCallVars, }}); }, }; } }; esLintObj.meta.messages[messageId] = message; module.exports = esLintObj;