Skip to content

Commit d3dd47f

Browse files
authored
feat: only exempt new-cap built-ins that reference the global (#21290)
* feat: only exempt `new-cap` built-ins that reference the global * add docs and globalThis test * add test
1 parent 5997825 commit d3dd47f

3 files changed

Lines changed: 377 additions & 13 deletions

File tree

‎docs/src/rules/new-cap.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ This rule requires constructor names to begin with a capital letter. Certain bui
2626
* `Symbol`
2727
* `BigInt`
2828

29+
These identifiers are exempt only when they reference the global built-in, either directly or as a property of the global object (`global`, `globalThis`, `self`, or `window`). `Date.UTC()` is exempt under the same condition.
30+
2931
Examples of **correct** code for this rule:
3032

3133
::: correct
@@ -36,6 +38,34 @@ Examples of **correct** code for this rule:
3638
function foo(arg) {
3739
return Boolean(arg);
3840
}
41+
42+
function bar(arg) {
43+
return globalThis.Boolean(arg);
44+
}
45+
46+
const time = Date.UTC(2000, 0);
47+
48+
const otherTime = globalThis.Date.UTC(2000, 0);
49+
```
50+
51+
:::
52+
53+
Examples of **incorrect** code for this rule:
54+
55+
::: incorrect
56+
57+
```js
58+
/*eslint new-cap: "error"*/
59+
60+
function String(value) {
61+
return value + "";
62+
}
63+
64+
const foo = String(42);
65+
66+
const Date = { UTC() { return 0; } };
67+
68+
const time = Date.UTC(2000, 0);
3969
```
4070

4171
:::

‎lib/rules/new-cap.js‎

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const astUtils = require("./utils/ast-utils");
1515
// Helpers
1616
//------------------------------------------------------------------------------
1717

18-
const CAPS_ALLOWED = [
18+
const CAPS_ALLOWED = new Set([
1919
"Array",
2020
"Boolean",
2121
"Date",
@@ -27,7 +27,9 @@ const CAPS_ALLOWED = [
2727
"String",
2828
"Symbol",
2929
"BigInt",
30-
];
30+
]);
31+
32+
const GLOBAL_OBJECT_NAMES = new Set(["global", "globalThis", "self", "window"]);
3133

3234
//------------------------------------------------------------------------------
3335
// Rule Definition
@@ -84,7 +86,7 @@ module.exports = {
8486
defaultOptions: [
8587
{
8688
capIsNew: true,
87-
capIsNewExceptions: CAPS_ALLOWED,
89+
capIsNewExceptions: [],
8890
newIsCap: true,
8991
newIsCapExceptions: [],
9092
properties: true,
@@ -106,10 +108,7 @@ module.exports = {
106108
? new RegExp(config.newIsCapExceptionPattern, "u")
107109
: null;
108110

109-
const capIsNewExceptions = new Set([
110-
...config.capIsNewExceptions,
111-
...CAPS_ALLOWED,
112-
]);
111+
const capIsNewExceptions = new Set(config.capIsNewExceptions);
113112
const capIsNewExceptionPattern = config.capIsNewExceptionPattern
114113
? new RegExp(config.capIsNewExceptionPattern, "u")
115114
: null;
@@ -155,6 +154,43 @@ module.exports = {
155154
return "upper";
156155
}
157156

157+
/**
158+
* Checks if the given node is a reference to the global object
159+
* @param {ASTNode} node Node to check
160+
* @returns {boolean} Returns true if the node references the global object
161+
*/
162+
function isGlobalObject(node) {
163+
return (
164+
node.type === "Identifier" &&
165+
GLOBAL_OBJECT_NAMES.has(node.name) &&
166+
sourceCode.isGlobalReference(node)
167+
);
168+
}
169+
170+
/**
171+
* Checks if the given node references a built-in of the global object,
172+
* such as `Date` or `globalThis.Date`
173+
* @param {ASTNode} node Node to check
174+
* @param {string} name Name of the built-in
175+
* @returns {boolean} Returns true if the node references the built-in of the global object
176+
*/
177+
function isGlobalBuiltIn(node, name) {
178+
const expression = astUtils.skipChainExpression(node);
179+
180+
if (expression.type === "MemberExpression") {
181+
return (
182+
astUtils.getStaticPropertyName(expression) === name &&
183+
isGlobalObject(expression.object)
184+
);
185+
}
186+
187+
return (
188+
expression.type === "Identifier" &&
189+
expression.name === name &&
190+
sourceCode.isGlobalReference(expression)
191+
);
192+
}
193+
158194
/**
159195
* Check if capitalization is allowed for a CallExpression
160196
* @param {Set<string>} allowedNames Set of allowed callee names
@@ -182,11 +218,8 @@ module.exports = {
182218
}
183219

184220
if (calleeName === "UTC") {
185-
// allow if callee is Date.UTC
186-
return (
187-
callee.object.type === "Identifier" &&
188-
callee.object.name === "Date"
189-
);
221+
// allow if callee is the global `Date.UTC`
222+
return isGlobalBuiltIn(callee.object, "Date");
190223
}
191224
}
192225

@@ -248,7 +281,9 @@ module.exports = {
248281
node,
249282
calleeName,
250283
capIsNewExceptionPattern,
251-
);
284+
) ||
285+
(CAPS_ALLOWED.has(calleeName) &&
286+
isGlobalBuiltIn(node.callee, calleeName));
252287

253288
if (!isAllowed) {
254289
report(node, "upper");

0 commit comments

Comments
 (0)