Skip to content

Commit c773c16

Browse files
himynameisdavefiskersindresorhus
authored
Add no-null rule (#636)
Co-authored-by: fisker Cheung <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent d986427 commit c773c16

File tree

11 files changed

+402
-16
lines changed

11 files changed

+402
-16
lines changed

docs/rules/no-null.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Disallow the use of the `null` literal.
2+
3+
Disallow the use of the `null` literal, to encourage using `undefined` instead.
4+
5+
## Fail
6+
7+
```js
8+
let foo = null;
9+
```
10+
11+
```js
12+
if (bar == null) {}
13+
```
14+
15+
## Pass
16+
17+
```js
18+
let foo;
19+
```
20+
21+
```js
22+
const foo = Object.create(null);
23+
```
24+
25+
```js
26+
if (foo === null) {}
27+
```
28+
29+
## Options
30+
31+
Type: `object`
32+
33+
### checkStrictEquality
34+
35+
Type: `boolean`\
36+
Default: `false`
37+
38+
Strict equality(`===`) and strict inequality(`!==`) is ignored by default.
39+
40+
#### Fail
41+
42+
```js
43+
// eslint unicorn/no-null: ["error", {"checkStrictEquality": true}]
44+
if (foo === null) {}
45+
```
46+
47+
## Why
48+
49+
- [“Intent to stop using `null` in my JS code”](https://github.com/sindresorhus/meta/issues/7).
50+
- [TypeScript coding guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined).
51+
- [ESLint rule proposal](https://github.com/eslint/eslint/issues/6701).
52+
- [Douglas Crockford](https://www.youtube.com/watch?v=PSGEjv3Tqo0&t=9m21s) on bottom values in JavaScript.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports = {
3838
'no-nested-ternary': 'off',
3939
'unicorn/no-nested-ternary': 'error',
4040
'unicorn/no-new-buffer': 'error',
41+
'unicorn/no-null': 'error',
4142
'unicorn/no-process-exit': 'error',
4243
'unicorn/no-unreadable-array-destructuring': 'error',
4344
'unicorn/no-unsafe-regex': 'off',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Configure it in `package.json`.
5454
"no-nested-ternary": "off",
5555
"unicorn/no-nested-ternary": "error",
5656
"unicorn/no-new-buffer": "error",
57+
"unicorn/no-null": "error",
5758
"unicorn/no-process-exit": "error",
5859
"unicorn/no-unreadable-array-destructuring": "error",
5960
"unicorn/no-unsafe-regex": "off",
@@ -110,6 +111,7 @@ Configure it in `package.json`.
110111
- [no-keyword-prefix](docs/rules/no-keyword-prefix.md) - Disallow identifiers starting with `new` or `class`.
111112
- [no-nested-ternary](docs/rules/no-nested-ternary.md) - Disallow nested ternary expressions. *(partly fixable)*
112113
- [no-new-buffer](docs/rules/no-new-buffer.md) - Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. *(fixable)*
114+
- [no-null](docs/rules/no-null.md) - Disallow the use of the `null` literal.
113115
- [no-process-exit](docs/rules/no-process-exit.md) - Disallow `process.exit()`.
114116
- [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) - Disallow unreadable array destructuring.
115117
- [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.

rules/expiring-todo-comments.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ const create = context => {
281281
if (dates.length > 1) {
282282
uses++;
283283
context.report({
284-
node: null,
285284
loc: comment.loc,
286285
messageId: MESSAGE_ID_AVOID_MULTIPLE_DATES,
287286
data: {
@@ -296,7 +295,6 @@ const create = context => {
296295
const shouldIgnore = options.ignoreDatesOnPullRequests && ci.isPR;
297296
if (!shouldIgnore && reachedDate(date)) {
298297
context.report({
299-
node: null,
300298
loc: comment.loc,
301299
messageId: MESSAGE_ID_EXPIRED_TODO,
302300
data: {
@@ -310,7 +308,6 @@ const create = context => {
310308
if (packageVersions.length > 1) {
311309
uses++;
312310
context.report({
313-
node: null,
314311
loc: comment.loc,
315312
messageId: MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS,
316313
data: {
@@ -330,7 +327,6 @@ const create = context => {
330327
const compare = semverComparisonForOperator(condition);
331328
if (packageVersion && compare(packageVersion, decidedPackageVersion)) {
332329
context.report({
333-
node: null,
334330
loc: comment.loc,
335331
messageId: MESSAGE_ID_REACHED_PACKAGE_VERSION,
336332
data: {
@@ -357,7 +353,6 @@ const create = context => {
357353

358354
if (trigger) {
359355
context.report({
360-
node: null,
361356
loc: comment.loc,
362357
messageId,
363358
data: {
@@ -382,7 +377,6 @@ const create = context => {
382377

383378
if (compare(targetPackageVersion, todoVersion)) {
384379
context.report({
385-
node: null,
386380
loc: comment.loc,
387381
messageId: MESSAGE_ID_VERSION_MATCHES,
388382
data: {
@@ -414,7 +408,6 @@ const create = context => {
414408

415409
if (compare(targetPackageEngineVersion, todoEngine)) {
416410
context.report({
417-
node: null,
418411
loc: comment.loc,
419412
messageId: MESSAGE_ID_ENGINE_MATCHES,
420413
data: {
@@ -439,7 +432,6 @@ const create = context => {
439432
if (parseArgument(testString).type !== 'unknowns') {
440433
uses++;
441434
context.report({
442-
node: null,
443435
loc: comment.loc,
444436
messageId: MESSAGE_ID_MISSING_AT_SYMBOL,
445437
data: {
@@ -457,7 +449,6 @@ const create = context => {
457449
if (parseArgument(withoutWhitespaces).type !== 'unknowns') {
458450
uses++;
459451
context.report({
460-
node: null,
461452
loc: comment.loc,
462453
messageId: MESSAGE_ID_REMOVE_WHITESPACES,
463454
data: {

rules/no-null.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
const getDocumentationUrl = require('./utils/get-documentation-url');
3+
const methodSelector = require('./utils/method-selector');
4+
5+
const ERROR_MESSAGE_ID = 'error';
6+
const SUGGESTION_REPLACE_MESSAGE_ID = 'replace';
7+
const SUGGESTION_REMOVE_MESSAGE_ID = 'remove';
8+
9+
const objectCreateSelector = methodSelector({
10+
object: 'Object',
11+
name: 'create',
12+
length: 1
13+
});
14+
15+
const selector = [
16+
`:not(${objectCreateSelector})`,
17+
'>',
18+
'Literal',
19+
'[raw="null"]'
20+
].join('');
21+
22+
const isLooseEqual = node => node.type === 'BinaryExpression' && ['==', '!='].includes(node.operator);
23+
const isStrictEqual = node => node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator);
24+
25+
const create = context => {
26+
const {checkStrictEquality} = {
27+
checkStrictEquality: false,
28+
...context.options[0]
29+
};
30+
31+
return {
32+
[selector]: node => {
33+
const problem = {
34+
node,
35+
messageId: ERROR_MESSAGE_ID
36+
};
37+
38+
/* istanbul ignore next */
39+
const {parent = {}} = node;
40+
41+
if (!checkStrictEquality && isStrictEqual(parent)) {
42+
return;
43+
}
44+
45+
const fix = fixer => fixer.replaceText(node, 'undefined');
46+
const replaceSuggestion = {
47+
messageId: SUGGESTION_REPLACE_MESSAGE_ID,
48+
fix
49+
};
50+
51+
if (isLooseEqual(parent)) {
52+
problem.fix = fix;
53+
} else if (parent.type === 'ReturnStatement' && parent.argument === node) {
54+
problem.suggest = [
55+
{
56+
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
57+
fix: fixer => fixer.remove(node)
58+
},
59+
replaceSuggestion
60+
];
61+
} else if (parent.type === 'VariableDeclarator' && parent.init === node && parent.parent.kind !== 'const') {
62+
problem.suggest = [
63+
{
64+
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
65+
fix: fixer => fixer.removeRange([parent.id.range[1], node.range[1]])
66+
},
67+
replaceSuggestion
68+
];
69+
} else {
70+
problem.suggest = [
71+
replaceSuggestion
72+
];
73+
}
74+
75+
context.report(problem);
76+
}
77+
};
78+
};
79+
80+
const schema = [
81+
{
82+
type: 'object',
83+
properties: {
84+
checkStrictEquality: {
85+
type: 'boolean',
86+
default: false
87+
}
88+
},
89+
additionalProperties: false
90+
}
91+
];
92+
93+
module.exports = {
94+
create,
95+
meta: {
96+
type: 'suggestion',
97+
docs: {
98+
url: getDocumentationUrl(__filename)
99+
},
100+
messages: {
101+
[ERROR_MESSAGE_ID]: 'Use `undefined` instead of `null`.',
102+
[SUGGESTION_REPLACE_MESSAGE_ID]: 'Replace `null` with `undefined`.',
103+
[SUGGESTION_REMOVE_MESSAGE_ID]: 'Remove `null`.'
104+
},
105+
schema,
106+
fixable: 'code'
107+
}
108+
};

rules/no-unused-properties.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ const create = context => {
139139
return {identifier: parent};
140140
}
141141

142-
return null;
142+
return undefined;
143143
}
144144

145145
if (parent.type === 'MemberExpression') {
@@ -152,7 +152,7 @@ const create = context => {
152152
return {identifier: parent};
153153
}
154154

155-
return null;
155+
return undefined;
156156
}
157157

158158
if (
@@ -163,7 +163,7 @@ const create = context => {
163163
return {identifier: parent};
164164
}
165165

166-
return null;
166+
return undefined;
167167
}
168168

169169
if (
@@ -174,7 +174,7 @@ const create = context => {
174174
return {identifier: parent};
175175
}
176176

177-
return null;
177+
return undefined;
178178
}
179179

180180
return reference;

rules/prefer-add-event-listener.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const create = context => {
5454
let isDisabled;
5555

5656
const nodeReturnsSomething = new WeakMap();
57-
let codePathInfo = null;
57+
let codePathInfo;
5858

5959
return {
6060
onCodePathStart(codePath, node) {

rules/prefer-reflect-apply.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const getDocumentationUrl = require('./utils/get-documentation-url');
44
const isLiteralValue = require('./utils/is-literal-value');
55

66
const isApplySignature = (argument1, argument2) => (
7+
// Please remove this file from `test/lint/lint.js` when fixing `null` issue
78
(isLiteralValue(argument1, null) ||
89
argument1.type === 'ThisExpression') &&
910
(argument2.type === 'ArrayExpression' ||

test/integration/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ list.run()
148148
const {line} = error2.eslintMessage;
149149

150150
console.error(chalk.gray(`${project.repository}/tree/master/${path.relative(destination, file.filePath)}#L${line}`));
151-
console.error(chalk.gray(JSON.stringify(error2.eslintMessage, null, 2)));
151+
console.error(chalk.gray(JSON.stringify(error2.eslintMessage, undefined, 2)));
152152
}
153153
}
154154
} else {

test/lint/lint.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const unicornRules = new Map(Object.entries(unicorn.rules));
1212
const cli = new CLIEngine({
1313
baseConfig: recommended,
1414
useEslintrc: false,
15-
fix
15+
fix,
16+
ignorePattern: [
17+
// This can't disable by `eslint-disable` before `xo` update
18+
'rules/prefer-reflect-apply.js'
19+
]
1620
});
1721

1822
cli.addPlugin('eslint-plugin-unicorn', unicorn);

0 commit comments

Comments
 (0)