Skip to content

Commit 50f5af3

Browse files
Add Object.create(null) exemption to no-null rule and tests
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/88c04cb4-f442-4fe2-8991-6a20aa3c6f9b Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com>
1 parent f7eebd6 commit 50f5af3

2 files changed

Lines changed: 104 additions & 5 deletions

File tree

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import type { TSESTree, TSESLint } from '@typescript-eslint/utils';
5+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
56

67
type MessageIds = 'error-usage-of-null';
78
type Options = [];
@@ -27,15 +28,29 @@ const noNullRule: TSESLint.RuleModule<MessageIds, Options> = {
2728
// Is it a "null" literal?
2829
if (node.value === null) {
2930
// Does the "null" appear in a comparison such as "if (x === null)"?
30-
let isComparison: boolean = false;
31-
if (node.parent && node.parent.type === 'BinaryExpression') {
31+
if (node.parent && node.parent.type === AST_NODE_TYPES.BinaryExpression) {
3232
const operator: string = node.parent.operator;
33-
isComparison = operator === '!==' || operator === '===' || operator === '!=' || operator === '==';
33+
if (operator === '!==' || operator === '===' || operator === '!=' || operator === '==') {
34+
return;
35+
}
3436
}
3537

36-
if (!isComparison) {
37-
context.report({ node, messageId: 'error-usage-of-null' });
38+
// Is this "Object.create(null)"? This is the correct pattern for creating
39+
// a dictionary object that does not inherit members from the Object prototype.
40+
if (
41+
node.parent &&
42+
node.parent.type === AST_NODE_TYPES.CallExpression &&
43+
node.parent.arguments[0] === node &&
44+
node.parent.callee.type === AST_NODE_TYPES.MemberExpression &&
45+
node.parent.callee.object.type === AST_NODE_TYPES.Identifier &&
46+
node.parent.callee.object.name === 'Object' &&
47+
node.parent.callee.property.type === AST_NODE_TYPES.Identifier &&
48+
node.parent.callee.property.name === 'create'
49+
) {
50+
return;
3851
}
52+
53+
context.report({ node, messageId: 'error-usage-of-null' });
3954
}
4055
}
4156
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { RuleTester } from '@typescript-eslint/rule-tester';
5+
6+
import { getRuleTesterWithoutProject } from './ruleTester';
7+
import { noNullRule } from '../no-null';
8+
9+
const ruleTester: RuleTester = getRuleTesterWithoutProject();
10+
11+
ruleTester.run('no-null', noNullRule, {
12+
invalid: [
13+
{
14+
// Assigning null to a variable
15+
code: 'let x = null;',
16+
errors: [{ messageId: 'error-usage-of-null' }]
17+
},
18+
{
19+
// Passing null as a function argument (not Object.create)
20+
code: 'foo(null);',
21+
errors: [{ messageId: 'error-usage-of-null' }]
22+
},
23+
{
24+
// Returning null
25+
code: 'function f() { return null; }',
26+
errors: [{ messageId: 'error-usage-of-null' }]
27+
},
28+
{
29+
// Using null in a ternary
30+
code: 'let x = true ? null : undefined;',
31+
errors: [{ messageId: 'error-usage-of-null' }]
32+
},
33+
{
34+
// Using null as a second argument to Object.create
35+
code: 'Object.create(proto, null);',
36+
errors: [{ messageId: 'error-usage-of-null' }]
37+
},
38+
{
39+
// Using null on a different method of Object
40+
code: 'Object.assign(null);',
41+
errors: [{ messageId: 'error-usage-of-null' }]
42+
},
43+
{
44+
// Using null with a different object's create method
45+
code: 'NotObject.create(null);',
46+
errors: [{ messageId: 'error-usage-of-null' }]
47+
},
48+
{
49+
// Computed property access: Object["create"](null) is NOT exempted
50+
code: 'Object["create"](null);',
51+
errors: [{ messageId: 'error-usage-of-null' }]
52+
}
53+
],
54+
valid: [
55+
{
56+
// Comparison with === is allowed
57+
code: 'if (x === null) {}'
58+
},
59+
{
60+
// Comparison with !== is allowed
61+
code: 'if (x !== null) {}'
62+
},
63+
{
64+
// Comparison with == is allowed
65+
code: 'if (x == null) {}'
66+
},
67+
{
68+
// Comparison with != is allowed
69+
code: 'if (x != null) {}'
70+
},
71+
{
72+
// Object.create(null) is allowed for creating prototype-less dictionary objects
73+
code: 'const dict = Object.create(null);'
74+
},
75+
{
76+
// Object.create(null) with type annotation
77+
code: 'const dict: Record<string, number> = Object.create(null);'
78+
},
79+
{
80+
// Object.create(null) inside a function
81+
code: 'function createDict() { return Object.create(null); }'
82+
}
83+
]
84+
});

0 commit comments

Comments
 (0)