123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- /**
- * @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<Node>} 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<Node>} 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<Node>} 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/mysticatea/eslint-plugin-node/blob/v11.1.0/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",
- })
- }
- }
- },
- }
- },
- }
|