/** * @author Nicholas C. Zakas * See LICENSE file in root directory for full license. */ "use strict" const path = require("path") const { READ, ReferenceTracker, getStringIfConstant } = require("eslint-utils") /** * Get the first char of the specified template element. * @param {TemplateLiteral} node The `TemplateLiteral` node to get. * @param {number} i The number of template elements to get first char. * @param {Set} sepNodes The nodes of `path.sep`. * @param {import("escope").Scope} globalScope The global scope object. * @param {string[]} outNextChars The array to collect chars. * @returns {void} */ function collectFirstCharsOfTemplateElement( node, i, sepNodes, globalScope, outNextChars ) { const element = node.quasis[i].value.cooked if (element == null) { return } if (element !== "") { outNextChars.push(element[0]) return } if (node.expressions.length > i) { collectFirstChars( node.expressions[i], sepNodes, globalScope, outNextChars ) } } /** * Get the first char of a given node. * @param {TemplateLiteral} node The `TemplateLiteral` node to get. * @param {Set} sepNodes The nodes of `path.sep`. * @param {import("escope").Scope} globalScope The global scope object. * @param {string[]} outNextChars The array to collect chars. * @returns {void} */ function collectFirstChars(node, sepNodes, globalScope, outNextChars) { switch (node.type) { case "AssignmentExpression": collectFirstChars(node.right, sepNodes, globalScope, outNextChars) break case "BinaryExpression": collectFirstChars(node.left, sepNodes, globalScope, outNextChars) break case "ConditionalExpression": collectFirstChars( node.consequent, sepNodes, globalScope, outNextChars ) collectFirstChars( node.alternate, sepNodes, globalScope, outNextChars ) break case "LogicalExpression": collectFirstChars(node.left, sepNodes, globalScope, outNextChars) collectFirstChars(node.right, sepNodes, globalScope, outNextChars) break case "SequenceExpression": collectFirstChars( node.expressions[node.expressions.length - 1], sepNodes, globalScope, outNextChars ) break case "TemplateLiteral": collectFirstCharsOfTemplateElement( node, 0, sepNodes, globalScope, outNextChars ) break case "Identifier": case "MemberExpression": if (sepNodes.has(node)) { outNextChars.push(path.sep) break } // fallthrough default: { const str = getStringIfConstant(node, globalScope) if (str) { outNextChars.push(str[0]) } } } } /** * Check if a char is a path separator or not. * @param {string} c The char to check. * @returns {boolean} `true` if the char is a path separator. */ function isPathSeparator(c) { return c === "/" || c === path.sep } /** * Check if the given Identifier node is followed by string concatenation with a * path separator. * @param {Identifier} node The `__dirname` or `__filename` node to check. * @param {Set} sepNodes The nodes of `path.sep`. * @param {import("escope").Scope} globalScope The global scope object. * @returns {boolean} `true` if the given Identifier node is followed by string * concatenation with a path separator. */ function isConcat(node, sepNodes, globalScope) { const parent = node.parent const nextChars = [] if ( parent.type === "BinaryExpression" && parent.operator === "+" && parent.left === node ) { collectFirstChars( parent.right, sepNodes, globalScope, /* out */ nextChars ) } else if (parent.type === "TemplateLiteral") { collectFirstCharsOfTemplateElement( parent, parent.expressions.indexOf(node) + 1, sepNodes, globalScope, /* out */ nextChars ) } return nextChars.some(isPathSeparator) } module.exports = { meta: { type: "suggestion", docs: { description: "disallow string concatenation with `__dirname` and `__filename`", category: "Possible Errors", recommended: false, url: "https://github.com/weiran-zsd/eslint-plugin-node/blob/HEAD/docs/rules/no-path-concat.md", }, fixable: null, schema: [], messages: { usePathFunctions: "Use path.join() or path.resolve() instead of string concatenation.", }, }, create(context) { return { "Program:exit"() { const globalScope = context.getScope() const tracker = new ReferenceTracker(globalScope) const sepNodes = new Set() // Collect `paht.sep` references for (const { node } of tracker.iterateCjsReferences({ path: { sep: { [READ]: true } }, })) { sepNodes.add(node) } for (const { node } of tracker.iterateEsmReferences({ path: { sep: { [READ]: true } }, })) { sepNodes.add(node) } // Verify `__dirname` and `__filename` for (const { node } of tracker.iterateGlobalReferences({ __dirname: { [READ]: true }, __filename: { [READ]: true }, })) { if (isConcat(node, sepNodes, globalScope)) { context.report({ node: node.parent, messageId: "usePathFunctions", }) } } }, } }, }