/** * @fileoverview Rule to enforce grouped require statements for Node.JS * @author Raphael Pigulla */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "disallow `require` calls to be mixed with regular variable declarations", category: "Node.js and CommonJS", recommended: false }, schema: [ { oneOf: [ { type: "boolean" }, { type: "object", properties: { grouping: { type: "boolean" }, allowCall: { type: "boolean" } }, additionalProperties: false } ] } ] }, create(context) { const options = context.options[0]; let grouping = false, allowCall = false; if (typeof options === "object") { grouping = options.grouping; allowCall = options.allowCall; } else { grouping = !!options; } /** * Returns the list of built-in modules. * * @returns {string[]} An array of built-in Node.js modules. */ function getBuiltinModules() { /* * This list is generated using: * `require("repl")._builtinLibs.concat('repl').sort()` * This particular list is as per nodejs v0.12.2 and iojs v0.7.1 */ return [ "assert", "buffer", "child_process", "cluster", "crypto", "dgram", "dns", "domain", "events", "fs", "http", "https", "net", "os", "path", "punycode", "querystring", "readline", "repl", "smalloc", "stream", "string_decoder", "tls", "tty", "url", "util", "v8", "vm", "zlib" ]; } const BUILTIN_MODULES = getBuiltinModules(); const DECL_REQUIRE = "require", DECL_UNINITIALIZED = "uninitialized", DECL_OTHER = "other"; const REQ_CORE = "core", REQ_FILE = "file", REQ_MODULE = "module", REQ_COMPUTED = "computed"; /** * Determines the type of a declaration statement. * @param {ASTNode} initExpression The init node of the VariableDeclarator. * @returns {string} The type of declaration represented by the expression. */ function getDeclarationType(initExpression) { if (!initExpression) { // "var x;" return DECL_UNINITIALIZED; } if (initExpression.type === "CallExpression" && initExpression.callee.type === "Identifier" && initExpression.callee.name === "require" ) { // "var x = require('util');" return DECL_REQUIRE; } if (allowCall && initExpression.type === "CallExpression" && initExpression.callee.type === "CallExpression" ) { // "var x = require('diagnose')('sub-module');" return getDeclarationType(initExpression.callee); } if (initExpression.type === "MemberExpression") { // "var x = require('glob').Glob;" return getDeclarationType(initExpression.object); } // "var x = 42;" return DECL_OTHER; } /** * Determines the type of module that is loaded via require. * @param {ASTNode} initExpression The init node of the VariableDeclarator. * @returns {string} The module type. */ function inferModuleType(initExpression) { if (initExpression.type === "MemberExpression") { // "var x = require('glob').Glob;" return inferModuleType(initExpression.object); } if (initExpression.arguments.length === 0) { // "var x = require();" return REQ_COMPUTED; } const arg = initExpression.arguments[0]; if (arg.type !== "Literal" || typeof arg.value !== "string") { // "var x = require(42);" return REQ_COMPUTED; } if (BUILTIN_MODULES.indexOf(arg.value) !== -1) { // "var fs = require('fs');" return REQ_CORE; } if (/^\.{0,2}\//.test(arg.value)) { // "var utils = require('./utils');" return REQ_FILE; } // "var async = require('async');" return REQ_MODULE; } /** * Check if the list of variable declarations is mixed, i.e. whether it * contains both require and other declarations. * @param {ASTNode} declarations The list of VariableDeclarators. * @returns {boolean} True if the declarations are mixed, false if not. */ function isMixed(declarations) { const contains = {}; declarations.forEach(declaration => { const type = getDeclarationType(declaration.init); contains[type] = true; }); return !!( contains[DECL_REQUIRE] && (contains[DECL_UNINITIALIZED] || contains[DECL_OTHER]) ); } /** * Check if all require declarations in the given list are of the same * type. * @param {ASTNode} declarations The list of VariableDeclarators. * @returns {boolean} True if the declarations are grouped, false if not. */ function isGrouped(declarations) { const found = {}; declarations.forEach(declaration => { if (getDeclarationType(declaration.init) === DECL_REQUIRE) { found[inferModuleType(declaration.init)] = true; } }); return Object.keys(found).length <= 1; } return { VariableDeclaration(node) { if (isMixed(node.declarations)) { context.report({ node, message: "Do not mix 'require' and other declarations." }); } else if (grouping && !isGrouped(node.declarations)) { context.report({ node, message: "Do not mix core, module, file and computed requires." }); } } }; } };