summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js
blob: 0eb5b9a3d772f15b6ee9285b324b7af6fdfabf68 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

"use strict";

const { dirname, join } = require("path");

const eslintBasePath = dirname(require.resolve("eslint"));

const noredeclarePath = join(eslintBasePath, "rules/no-redeclare.js");
const baseRule = require(noredeclarePath);
const astUtils = require(join(eslintBasePath, "rules/utils/ast-utils.js"));

// Hack alert: our eslint env is pretty confused about `require` and
// `loader` for devtools modules - so ignore it for now.
// See bug 1812547
const gIgnoredImports = new Set(["loader", "require"]);

/**
 * Create a trap for a call to `report` that the original rule is
 * trying to make on `context`.
 *
 * Returns a function that forwards to `report` but provides a fixer
 * for redeclared imports that just removes those imports.
 *
 * @return {function}
 */
function trapReport(context) {
  return function (obj) {
    let declarator = obj.node.parent;
    while (
      declarator &&
      declarator.parent &&
      declarator.type != "VariableDeclarator"
    ) {
      declarator = declarator.parent;
    }
    if (
      declarator &&
      declarator.type == "VariableDeclarator" &&
      declarator.id.type == "ObjectPattern" &&
      declarator.init.type == "CallExpression"
    ) {
      let initialization = declarator.init;
      if (
        astUtils.isSpecificMemberAccess(
          initialization.callee,
          "ChromeUtils",
          /^import(ESModule|)$/
        )
      ) {
        // Hack alert: our eslint env is pretty confused about `require` and
        // `loader` for devtools modules - so ignore it for now.
        // See bug 1812547
        if (gIgnoredImports.has(obj.node.name)) {
          return;
        }
        // OK, we've got something we can fix. But we should be careful in case
        // there are multiple imports being destructured.
        // Do the easy (and common) case first - just one property:
        if (declarator.id.properties.length == 1) {
          context.report({
            node: declarator.parent,
            messageId: "duplicateImport",
            data: {
              name: declarator.id.properties[0].key.name,
            },
            fix(fixer) {
              return fixer.remove(declarator.parent);
            },
          });
          return;
        }

        // OK, figure out which import is duplicated here:
        let node = obj.node.parent;
        // Then remove a comma after it, or a comma before
        // if there's no comma after it.
        let sourceCode = context.getSourceCode();
        let rangeToRemove = node.range;
        let tokenAfter = sourceCode.getTokenAfter(node);
        let tokenBefore = sourceCode.getTokenBefore(node);
        if (astUtils.isCommaToken(tokenAfter)) {
          rangeToRemove[1] = tokenAfter.range[1];
        } else if (astUtils.isCommaToken(tokenBefore)) {
          rangeToRemove[0] = tokenBefore.range[0];
        }
        context.report({
          node,
          messageId: "duplicateImport",
          data: {
            name: node.key.name,
          },
          fix(fixer) {
            return fixer.removeRange(rangeToRemove);
          },
        });
        return;
      }
    }
    if (context.options[0]?.errorForNonImports) {
      // Report the result from no-redeclare - we can't autofix it.
      // This can happen for other redeclaration issues, e.g. naming
      // variables in a way that conflicts with builtins like "URL" or
      // "escape".
      context.report(obj);
    }
  };
}

module.exports = {
  meta: {
    docs: {
      url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/no-redeclare-with-import-autofix.html",
    },
    messages: {
      ...baseRule.meta.messages,
      duplicateImport:
        "The import of '{{ name }}' is redundant with one set up earlier (e.g. head.js or the browser window environment). It should be removed.",
    },
    schema: [
      {
        type: "object",
        properties: {
          errorForNonImports: {
            type: "boolean",
            default: true,
          },
        },
        additionalProperties: false,
      },
    ],
    type: "suggestion",
    fixable: "code",
  },

  create(context) {
    // Test modules get the browser env applied wrongly in some cases,
    // don't try and remove imports there. This works out of the box
    // for sys.mjs modules because eslint won't check builtinGlobals
    // for the no-redeclare rule.
    if (context.getFilename().endsWith(".jsm")) {
      return {};
    }
    let newOptions = [{ builtinGlobals: true }];
    const contextForBaseRule = Object.create(context, {
      report: {
        value: trapReport(context),
        writable: false,
      },
      options: {
        value: newOptions,
      },
    });
    return baseRule.create(contextForBaseRule);
  },
};