@admc.com/eslint-plugin-sn
Version:
ESLint plugin for ServiceNow scriptlets
97 lines (89 loc) • 4.17 kB
JavaScript
;
/**
* 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;