-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
/
no-console.js
207 lines (181 loc) · 7.17 KB
/
no-console.js
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
* @fileoverview Rule to flag use of console object
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Disallow the use of `console`",
recommended: false,
url: "https://eslint.org/docs/latest/rules/no-console"
},
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
}
},
additionalProperties: false
}
],
hasSuggestions: true,
messages: {
unexpected: "Unexpected console statement.",
removeConsole: "Remove the console.{{ propertyName }}()."
}
},
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
const sourceCode = context.sourceCode;
/**
* Checks whether the given reference is 'console' or not.
* @param {eslint-scope.Reference} reference The reference to check.
* @returns {boolean} `true` if the reference is 'console'.
*/
function isConsole(reference) {
const id = reference.identifier;
return id && id.name === "console";
}
/**
* Checks whether the property name of the given MemberExpression node
* is allowed by options or not.
* @param {ASTNode} node The MemberExpression node to check.
* @returns {boolean} `true` if the property name of the node is allowed.
*/
function isAllowed(node) {
const propertyName = astUtils.getStaticPropertyName(node);
return propertyName && allowed.includes(propertyName);
}
/**
* Checks whether the given reference is a member access which is not
* allowed by options or not.
* @param {eslint-scope.Reference} reference The reference to check.
* @returns {boolean} `true` if the reference is a member access which
* is not allowed by options.
*/
function isMemberAccessExceptAllowed(reference) {
const node = reference.identifier;
const parent = node.parent;
return (
parent.type === "MemberExpression" &&
parent.object === node &&
!isAllowed(parent)
);
}
/**
* Checks if removing the ExpressionStatement node will cause ASI to
* break.
* eg.
* foo()
* console.log();
* [1, 2, 3].forEach(a => doSomething(a))
*
* Removing the console.log(); statement should leave two statements, but
* here the two statements will become one because [ causes continuation after
* foo().
* @param {ASTNode} node The ExpressionStatement node to check.
* @returns {boolean} `true` if ASI will break after removing the ExpressionStatement
* node.
*/
function maybeAsiHazard(node) {
const SAFE_TOKENS_BEFORE = /^[:;{]$/u; // One of :;{
const UNSAFE_CHARS_AFTER = /^[-[(/+`]/u; // One of [(/+-`
const tokenBefore = sourceCode.getTokenBefore(node);
const tokenAfter = sourceCode.getTokenAfter(node);
return (
Boolean(tokenAfter) &&
UNSAFE_CHARS_AFTER.test(tokenAfter.value) &&
tokenAfter.value !== "++" &&
tokenAfter.value !== "--" &&
Boolean(tokenBefore) &&
!SAFE_TOKENS_BEFORE.test(tokenBefore.value)
);
}
/**
* Checks if the MemberExpression node's parent.parent.parent is a
* Program, BlockStatement, StaticBlock, or SwitchCase node. This check
* is necessary to avoid providing a suggestion that might cause a syntax error.
*
* eg. if (a) console.log(b), removing console.log() here will lead to a
* syntax error.
* if (a) { console.log(b) }, removing console.log() here is acceptable.
*
* Additionally, it checks if the callee of the CallExpression node is
* the node itself.
*
* eg. foo(console.log), cannot provide a suggestion here.
* @param {ASTNode} node The MemberExpression node to check.
* @returns {boolean} `true` if a suggestion can be provided for a node.
*/
function canProvideSuggestions(node) {
return (
node.parent.type === "CallExpression" &&
node.parent.callee === node &&
node.parent.parent.type === "ExpressionStatement" &&
astUtils.STATEMENT_LIST_PARENTS.has(node.parent.parent.parent.type) &&
!maybeAsiHazard(node.parent.parent)
);
}
/**
* Reports the given reference as a violation.
* @param {eslint-scope.Reference} reference The reference to report.
* @returns {void}
*/
function report(reference) {
const node = reference.identifier.parent;
const propertyName = astUtils.getStaticPropertyName(node);
context.report({
node,
loc: node.loc,
messageId: "unexpected",
suggest: canProvideSuggestions(node)
? [{
messageId: "removeConsole",
data: { propertyName },
fix(fixer) {
return fixer.remove(node.parent.parent);
}
}]
: []
});
}
return {
"Program:exit"(node) {
const scope = sourceCode.getScope(node);
const consoleVar = astUtils.getVariableByName(scope, "console");
const shadowed = consoleVar && consoleVar.defs.length > 0;
/*
* 'scope.through' includes all references to undefined
* variables. If the variable 'console' is not defined, it uses
* 'scope.through'.
*/
const references = consoleVar
? consoleVar.references
: scope.through.filter(isConsole);
if (!shadowed) {
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
}
}
};
}
};