summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js
blob: ffd9bc95660667c249da6a5dd1be82e49586881e (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
/**
 * @fileoverview Reject use of instanceof against DOM interfaces
 *
 * 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 fs = require("fs");

const { maybeGetMemberPropertyName } = require("../helpers");

const privilegedGlobals = Object.keys(
  require("../environments/privileged.js").globals
);

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------

/**
 * Whether an identifier is defined by eslint configuration.
 * `env: { browser: true }` or `globals: []` for example.
 * @param {import("eslint-scope").Scope} currentScope
 * @param {import("estree").Identifier} id
 */
function refersToEnvironmentGlobals(currentScope, id) {
  const reference = currentScope.references.find(ref => ref.identifier === id);
  const { resolved } = reference || {};
  if (!resolved) {
    return false;
  }

  // No definition in script files; defined via .eslintrc
  return resolved.scope.type === "global" && resolved.defs.length === 0;
}

/**
 * Whether a node points to a DOM interface.
 * Includes direct references to interfaces objects and also indirect references
 * via property access.
 * OS.File and lazy.(Foo) are explicitly excluded.
 *
 * @example HTMLElement
 * @example win.HTMLElement
 * @example iframe.contentWindow.HTMLElement
 * @example foo.HTMLElement
 *
 * @param {import("eslint-scope").Scope} currentScope
 * @param {import("estree").Node} node
 */
function pointsToDOMInterface(currentScope, node) {
  if (node.type === "MemberExpression") {
    const objectName = maybeGetMemberPropertyName(node.object);
    if (objectName === "lazy") {
      // lazy.Foo is probably a non-IDL import.
      return false;
    }
    if (objectName === "OS" && node.property.name === "File") {
      // OS.File is an exception that is not a Web IDL interface
      return false;
    }
    // For `win.Foo`, `iframe.contentWindow.Foo`, or such.
    return privilegedGlobals.includes(node.property.name);
  }

  if (
    node.type === "Identifier" &&
    refersToEnvironmentGlobals(currentScope, node)
  ) {
    return privilegedGlobals.includes(node.name);
  }

  return false;
}

/**
 * @param {import("eslint").Rule.RuleContext} context
 */
function isChromeContext(context) {
  const filename = context.getFilename();
  const isChromeFileName =
    filename.endsWith(".sys.mjs") || filename.endsWith(".jsm");
  if (isChromeFileName) {
    return true;
  }

  if (filename.endsWith(".xhtml")) {
    // Treat scripts in XUL files as chrome scripts
    // Note: readFile is needed as getSourceCode() only gives JS blocks
    return fs.readFileSync(filename).includes("there.is.only.xul");
  }

  // Treat scripts as chrome privileged when using:
  // 1. ChromeUtils, but not SpecialPowers.ChromeUtils
  // 2. BrowserTestUtils, PlacesUtils
  // 3. document.createXULElement
  // 4. loader.lazyRequireGetter
  // 5. Services.foo, but not SpecialPowers.Services.foo
  // 6. evalInSandbox
  const source = context.getSourceCode().text;
  return !!source.match(
    /(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/
  );
}

module.exports = {
  meta: {
    docs: {
      url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-isInstance.html",
    },
    fixable: "code",
    messages: {
      preferIsInstance:
        "Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " +
        "since the latter will return false when the object is created from a different context.",
    },
    schema: [],
    type: "problem",
  },
  /**
   * @param {import("eslint").Rule.RuleContext} context
   */
  create(context) {
    if (!isChromeContext(context)) {
      return {};
    }

    return {
      BinaryExpression(node) {
        const { operator, right } = node;
        if (
          operator === "instanceof" &&
          pointsToDOMInterface(context.getScope(), right)
        ) {
          context.report({
            node,
            messageId: "preferIsInstance",
            fix(fixer) {
              const sourceCode = context.getSourceCode();
              return fixer.replaceText(
                node,
                `${sourceCode.getText(right)}.isInstance(${sourceCode.getText(
                  node.left
                )})`
              );
            },
          });
        }
      },
    };
  },
};