Skip to content

Commit d92c810

Browse files
Only flag empty {} for null-prototype-dictionaries; allow __proto__: null in no-null
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/1d063584-7336-4ab7-b4d0-e2a46bebba49 Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com>
1 parent 3650733 commit d92c810

4 files changed

Lines changed: 35 additions & 36 deletions

File tree

eslint/eslint-plugin/src/no-null.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ const noNullRule: TSESLint.RuleModule<MessageIds, Options> = {
5050
return;
5151
}
5252

53+
// Is this "__proto__: null" inside an object literal? This is used to create
54+
// an object literal that does not inherit from Object.prototype.
55+
if (
56+
node.parent &&
57+
node.parent.type === AST_NODE_TYPES.Property &&
58+
!node.parent.computed &&
59+
((node.parent.key.type === AST_NODE_TYPES.Identifier &&
60+
node.parent.key.name === '__proto__') ||
61+
(node.parent.key.type === AST_NODE_TYPES.Literal &&
62+
node.parent.key.value === '__proto__'))
63+
) {
64+
return;
65+
}
66+
5367
context.report({ node, messageId: 'error-usage-of-null' });
5468
}
5569
}

eslint/eslint-plugin/src/null-prototype-dictionaries.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type { TSESTree, TSESLint, ParserServices } from '@typescript-eslint/utils';
55
import type * as ts from 'typescript';
66

7-
type MessageIds = 'error-empty-object-literal-dictionary' | 'error-missing-null-prototype';
7+
type MessageIds = 'error-empty-object-literal-dictionary';
88
type Options = [];
99

1010
const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> = {
@@ -17,10 +17,7 @@ const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> =
1717
' instead of an empty object literal. This avoids prototype pollution, collisions with' +
1818
' Object.prototype members such as "toString", and enables higher performance since runtimes' +
1919
' such as V8 process Object.create(null) as opting out of having a hidden class and going' +
20-
' directly to dictionary mode.',
21-
'error-missing-null-prototype':
22-
'Dictionary object literals typed as Record<string, T> should include "__proto__: null"' +
23-
' to avoid prototype pollution and collisions with Object.prototype members such as "toString".'
20+
' directly to dictionary mode.'
2421
},
2522
schema: [],
2623
docs: {
@@ -83,31 +80,12 @@ const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> =
8380
return;
8481
}
8582

86-
// For empty object literals, recommend Object.create(null) which is more performant
83+
// Only flag empty object literals; non-empty literals are allowed for now
8784
if (node.properties.length === 0) {
8885
context.report({
8986
node,
9087
messageId: 'error-empty-object-literal-dictionary'
9188
});
92-
return;
93-
}
94-
95-
// For non-empty object literals, check whether "__proto__: null" is present
96-
const hasNullProto: boolean = node.properties.some(
97-
(prop) =>
98-
prop.type === 'Property' &&
99-
!prop.computed &&
100-
((prop.key.type === 'Identifier' && prop.key.name === '__proto__') ||
101-
(prop.key.type === 'Literal' && prop.key.value === '__proto__')) &&
102-
prop.value.type === 'Literal' &&
103-
prop.value.value === null
104-
);
105-
106-
if (!hasNullProto) {
107-
context.report({
108-
node,
109-
messageId: 'error-missing-null-prototype'
110-
});
11189
}
11290
}
11391
};

eslint/eslint-plugin/src/test/no-null.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ ruleTester.run('no-null', noNullRule, {
4949
// Computed property access: Object["create"](null) is NOT exempted
5050
code: 'Object["create"](null);',
5151
errors: [{ messageId: 'error-usage-of-null' }]
52+
},
53+
{
54+
// null on a non-__proto__ property is still flagged
55+
code: 'const x = { foo: null };',
56+
errors: [{ messageId: 'error-usage-of-null' }]
5257
}
5358
],
5459
valid: [
@@ -79,6 +84,14 @@ ruleTester.run('no-null', noNullRule, {
7984
{
8085
// Object.create(null) inside a function
8186
code: 'function createDict() { return Object.create(null); }'
87+
},
88+
{
89+
// __proto__: null is allowed in object literals
90+
code: 'const obj = { __proto__: null, a: 1 };'
91+
},
92+
{
93+
// __proto__: null as string key is also allowed
94+
code: 'const obj = { "__proto__": null, a: 1 };'
8295
}
8396
]
8497
});

eslint/eslint-plugin/src/test/null-prototype-dictionaries.test.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,6 @@ ruleTester.run('null-prototype-dictionaries', nullPrototypeDictionariesRule, {
3232
// Return value from function
3333
code: 'function f(): Record<string, number> { return {}; }',
3434
errors: [{ messageId: 'error-empty-object-literal-dictionary' }]
35-
},
36-
{
37-
// Non-empty object literal without __proto__: null
38-
code: 'const dict: Record<string, string> = { a: "hello" };',
39-
errors: [{ messageId: 'error-missing-null-prototype' }]
40-
},
41-
{
42-
// Non-empty object literal with __proto__ set to something other than null
43-
code: 'const dict: Record<string, string> = { __proto__: Object.prototype, a: "hello" };',
44-
errors: [{ messageId: 'error-missing-null-prototype' }]
4535
}
4636
],
4737
valid: [
@@ -50,7 +40,11 @@ ruleTester.run('null-prototype-dictionaries', nullPrototypeDictionariesRule, {
5040
code: 'const dict: Record<string, number> = Object.create(null);'
5141
},
5242
{
53-
// Correct pattern: non-empty literal with __proto__: null
43+
// Non-empty object literal is allowed for now
44+
code: 'const dict: Record<string, string> = { a: "hello" };'
45+
},
46+
{
47+
// Non-empty literal with __proto__: null is also fine
5448
code: 'const dict: Record<string, string> = { __proto__: null, a: "hello" };'
5549
},
5650
{

0 commit comments

Comments
 (0)