eslint 4.4.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ /**
2
+ * @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
3
+ * @author Teddy Katz
4
+ */
5
+
6
+ "use strict";
7
+
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const assert = require("assert");
13
+ const ruleFixer = require("./util/rule-fixer");
14
+
15
+ //------------------------------------------------------------------------------
16
+ // Typedefs
17
+ //------------------------------------------------------------------------------
18
+
19
+ /**
20
+ * An error message description
21
+ * @typedef {Object} MessageDescriptor
22
+ * @property {ASTNode} [node] The reported node
23
+ * @property {Location} loc The location of the problem.
24
+ * @property {string} message The problem message.
25
+ * @property {Object} [data] Optional data to use to fill in placeholders in the
26
+ * message.
27
+ * @property {Function} [fix] The function to call that creates a fix command.
28
+ */
29
+
30
+ //------------------------------------------------------------------------------
31
+ // Module Definition
32
+ //------------------------------------------------------------------------------
33
+
34
+
35
+ /**
36
+ * Translates a multi-argument context.report() call into a single object argument call
37
+ * @param {...*} arguments A list of arguments passed to `context.report`
38
+ * @returns {MessageDescriptor} A normalized object containing report information
39
+ */
40
+ function normalizeMultiArgReportCall() {
41
+
42
+ // If there is one argument, it is considered to be a new-style call already.
43
+ if (arguments.length === 1) {
44
+ return arguments[0];
45
+ }
46
+
47
+ // If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
48
+ if (typeof arguments[1] === "string") {
49
+ return {
50
+ node: arguments[0],
51
+ message: arguments[1],
52
+ data: arguments[2],
53
+ fix: arguments[3]
54
+ };
55
+ }
56
+
57
+ // Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
58
+ return {
59
+ node: arguments[0],
60
+ loc: arguments[1],
61
+ message: arguments[2],
62
+ data: arguments[3],
63
+ fix: arguments[4]
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Asserts that either a loc or a node was provided, and the node is valid if it was provided.
69
+ * @param {MessageDescriptor} descriptor A descriptor to validate
70
+ * @returns {void}
71
+ * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
72
+ */
73
+ function assertValidNodeInfo(descriptor) {
74
+ if (descriptor.node) {
75
+ assert(typeof descriptor.node === "object", "Node must be an object");
76
+ } else {
77
+ assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
83
+ * @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
84
+ * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
85
+ * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
86
+ */
87
+ function normalizeReportLoc(descriptor) {
88
+ if (descriptor.loc) {
89
+ if (descriptor.loc.start) {
90
+ return descriptor.loc;
91
+ }
92
+ return { start: descriptor.loc, end: null };
93
+ }
94
+ return descriptor.node.loc;
95
+ }
96
+
97
+ /**
98
+ * Interpolates data placeholders in report messages
99
+ * @param {MessageDescriptor} descriptor The report message descriptor.
100
+ * @returns {string} The interpolated message for the descriptor
101
+ */
102
+ function normalizeMessagePlaceholders(descriptor) {
103
+ if (!descriptor.data) {
104
+ return descriptor.message;
105
+ }
106
+ return descriptor.message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
107
+ if (term in descriptor.data) {
108
+ return descriptor.data[term];
109
+ }
110
+
111
+ return fullMatch;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Compares items in a fixes array by range.
117
+ * @param {Fix} a The first message.
118
+ * @param {Fix} b The second message.
119
+ * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
120
+ * @private
121
+ */
122
+ function compareFixesByRange(a, b) {
123
+ return a.range[0] - b.range[0] || a.range[1] - b.range[1];
124
+ }
125
+
126
+ /**
127
+ * Merges the given fixes array into one.
128
+ * @param {Fix[]} fixes The fixes to merge.
129
+ * @param {SourceCode} sourceCode The source code object to get the text between fixes.
130
+ * @returns {{text: string, range: [number, number]}} The merged fixes
131
+ */
132
+ function mergeFixes(fixes, sourceCode) {
133
+ if (fixes.length === 0) {
134
+ return null;
135
+ }
136
+ if (fixes.length === 1) {
137
+ return fixes[0];
138
+ }
139
+
140
+ fixes.sort(compareFixesByRange);
141
+
142
+ const originalText = sourceCode.text;
143
+ const start = fixes[0].range[0];
144
+ const end = fixes[fixes.length - 1].range[1];
145
+ let text = "";
146
+ let lastPos = Number.MIN_SAFE_INTEGER;
147
+
148
+ for (const fix of fixes) {
149
+ assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
150
+
151
+ if (fix.range[0] >= 0) {
152
+ text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
153
+ }
154
+ text += fix.text;
155
+ lastPos = fix.range[1];
156
+ }
157
+ text += originalText.slice(Math.max(0, start, lastPos), end);
158
+
159
+ return { range: [start, end], text };
160
+ }
161
+
162
+ /**
163
+ * Gets one fix object from the given descriptor.
164
+ * If the descriptor retrieves multiple fixes, this merges those to one.
165
+ * @param {MessageDescriptor} descriptor The report descriptor.
166
+ * @param {SourceCode} sourceCode The source code object to get text between fixes.
167
+ * @returns {({text: string, range: [number, number]}|null)} The fix for the descriptor
168
+ */
169
+ function normalizeFixes(descriptor, sourceCode) {
170
+ if (typeof descriptor.fix !== "function") {
171
+ return null;
172
+ }
173
+
174
+ // @type {null | Fix | Fix[] | IterableIterator<Fix>}
175
+ const fix = descriptor.fix(ruleFixer);
176
+
177
+ // Merge to one.
178
+ if (fix && Symbol.iterator in fix) {
179
+ return mergeFixes(Array.from(fix), sourceCode);
180
+ }
181
+ return fix;
182
+ }
183
+
184
+ /**
185
+ * Creates information about the report from a descriptor
186
+ * @param {{
187
+ * ruleId: string,
188
+ * severity: (0|1|2),
189
+ * node: (ASTNode|null),
190
+ * message: string,
191
+ * loc: {start: SourceLocation, end: (SourceLocation|null)},
192
+ * fix: ({text: string, range: [number, number]}|null),
193
+ * sourceLines: string[]
194
+ * }} options Information about the problem
195
+ * @returns {function(...args): {
196
+ * ruleId: string,
197
+ * severity: (0|1|2),
198
+ * message: string,
199
+ * line: number,
200
+ * column: number,
201
+ * endLine: (number|undefined),
202
+ * endColumn: (number|undefined),
203
+ * nodeType: (string|null),
204
+ * source: string,
205
+ * fix: ({text: string, range: [number, number]}|null)
206
+ * }} Information about the report
207
+ */
208
+ function createProblem(options) {
209
+ const problem = {
210
+ ruleId: options.ruleId,
211
+ severity: options.severity,
212
+ message: options.message,
213
+ line: options.loc.start.line,
214
+ column: options.loc.start.column + 1,
215
+ nodeType: options.node && options.node.type || null,
216
+ source: options.sourceLines[options.loc.start.line - 1] || ""
217
+ };
218
+
219
+ if (options.loc.end) {
220
+ problem.endLine = options.loc.end.line;
221
+ problem.endColumn = options.loc.end.column + 1;
222
+ }
223
+
224
+ if (options.fix) {
225
+ problem.fix = options.fix;
226
+ }
227
+
228
+ return problem;
229
+ }
230
+
231
+ /**
232
+ * Returns a function that converts the arguments of a `context.report` call from a rule into a reported
233
+ * problem for the Node.js API.
234
+ * @param {{ruleId: string, severity: number, sourceCode: SourceCode}} metadata Metadata for the reported problem
235
+ * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
236
+ * @returns {function(...args): {
237
+ * ruleId: string,
238
+ * severity: (0|1|2),
239
+ * message: string,
240
+ * line: number,
241
+ * column: number,
242
+ * endLine: (number|undefined),
243
+ * endColumn: (number|undefined),
244
+ * nodeType: (string|null),
245
+ * source: string,
246
+ * fix: ({text: string, range: [number, number]}|null)
247
+ * }}
248
+ * Information about the report
249
+ */
250
+
251
+ module.exports = function createReportTranslator(metadata) {
252
+
253
+ /*
254
+ * `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
255
+ * The report translator itself (i.e. the function that `createReportTranslator` returns) gets
256
+ * called every time a rule reports a problem, which happens much less frequently (usually, the vast
257
+ * majority of rules don't report any problems for a given file).
258
+ */
259
+ return function() {
260
+ const descriptor = normalizeMultiArgReportCall.apply(null, arguments);
261
+
262
+ assertValidNodeInfo(descriptor);
263
+
264
+ return createProblem({
265
+ ruleId: metadata.ruleId,
266
+ severity: metadata.severity,
267
+ node: descriptor.node,
268
+ message: normalizeMessagePlaceholders(descriptor),
269
+ loc: normalizeReportLoc(descriptor),
270
+ fix: normalizeFixes(descriptor, metadata.sourceCode),
271
+ sourceLines: metadata.sourceCode.lines
272
+ });
273
+ };
274
+ };
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @fileoverview enforce consistent line breaks inside function parentheses
3
+ * @author Teddy Katz
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const astUtils = require("../ast-utils");
12
+
13
+ //------------------------------------------------------------------------------
14
+ // Rule Definition
15
+ //------------------------------------------------------------------------------
16
+
17
+ module.exports = {
18
+ meta: {
19
+ docs: {
20
+ description: "enforce consistent line breaks inside function parentheses",
21
+ category: "Stylistic Issues",
22
+ recommended: false
23
+ },
24
+ fixable: "whitespace",
25
+ schema: [
26
+ {
27
+ oneOf: [
28
+ {
29
+ enum: ["always", "never", "consistent", "multiline"]
30
+ },
31
+ {
32
+ type: "object",
33
+ properties: {
34
+ minItems: {
35
+ type: "integer",
36
+ minimum: 0
37
+ }
38
+ },
39
+ additionalProperties: false
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ },
45
+
46
+ create(context) {
47
+ const sourceCode = context.getSourceCode();
48
+ const rawOption = context.options[0] || "multiline";
49
+ const multilineOption = rawOption === "multiline";
50
+ const consistentOption = rawOption === "consistent";
51
+ let minItems;
52
+
53
+ if (typeof rawOption === "object") {
54
+ minItems = rawOption.minItems;
55
+ } else if (rawOption === "always") {
56
+ minItems = 0;
57
+ } else if (rawOption === "never") {
58
+ minItems = Infinity;
59
+ } else {
60
+ minItems = null;
61
+ }
62
+
63
+ //----------------------------------------------------------------------
64
+ // Helpers
65
+ //----------------------------------------------------------------------
66
+
67
+ /**
68
+ * Determines whether there should be newlines inside function parens
69
+ * @param {ASTNode[]} elements The arguments or parameters in the list
70
+ * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
71
+ * @returns {boolean} `true` if there should be newlines inside the function parens
72
+ */
73
+ function shouldHaveNewlines(elements, hasLeftNewline) {
74
+ if (multilineOption) {
75
+ return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
76
+ }
77
+ if (consistentOption) {
78
+ return hasLeftNewline;
79
+ }
80
+ return elements.length >= minItems;
81
+ }
82
+
83
+ /**
84
+ * Validates a list of arguments or parameters
85
+ * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
86
+ * @param {ASTNode[]} elements The arguments or parameters in the list
87
+ * @returns {void}
88
+ */
89
+ function validateParens(parens, elements) {
90
+ const leftParen = parens.leftParen;
91
+ const rightParen = parens.rightParen;
92
+ const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
93
+ const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
94
+ const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
95
+ const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
96
+ const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
97
+
98
+ if (hasLeftNewline && !needsNewlines) {
99
+ context.report({
100
+ node: leftParen,
101
+ message: "Unexpected newline after '('.",
102
+ fix(fixer) {
103
+ return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
104
+
105
+ // If there is a comment between the ( and the first element, don't do a fix.
106
+ ? null
107
+ : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
108
+ }
109
+ });
110
+ } else if (!hasLeftNewline && needsNewlines) {
111
+ context.report({
112
+ node: leftParen,
113
+ message: "Expected a newline after '('.",
114
+ fix: fixer => fixer.insertTextAfter(leftParen, "\n")
115
+ });
116
+ }
117
+
118
+ if (hasRightNewline && !needsNewlines) {
119
+ context.report({
120
+ node: rightParen,
121
+ message: "Unexpected newline before ')'.",
122
+ fix(fixer) {
123
+ return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
124
+
125
+ // If there is a comment between the last element and the ), don't do a fix.
126
+ ? null
127
+ : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
128
+ }
129
+ });
130
+ } else if (!hasRightNewline && needsNewlines) {
131
+ context.report({
132
+ node: rightParen,
133
+ message: "Expected a newline before ')'.",
134
+ fix: fixer => fixer.insertTextBefore(rightParen, "\n")
135
+ });
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Gets the left paren and right paren tokens of a node.
141
+ * @param {ASTNode} node The node with parens
142
+ * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
143
+ * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
144
+ * with a single parameter)
145
+ */
146
+ function getParenTokens(node) {
147
+ switch (node.type) {
148
+ case "NewExpression":
149
+ if (!node.arguments.length && !(
150
+ astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
151
+ astUtils.isClosingParenToken(sourceCode.getLastToken(node))
152
+ )) {
153
+
154
+ // If the NewExpression does not have parens (e.g. `new Foo`), return null.
155
+ return null;
156
+ }
157
+
158
+ // falls through
159
+
160
+ case "CallExpression":
161
+ return {
162
+ leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
163
+ rightParen: sourceCode.getLastToken(node)
164
+ };
165
+
166
+ case "FunctionDeclaration":
167
+ case "FunctionExpression": {
168
+ const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
169
+ const rightParen = node.params.length
170
+ ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
171
+ : sourceCode.getTokenAfter(leftParen);
172
+
173
+ return { leftParen, rightParen };
174
+ }
175
+
176
+ case "ArrowFunctionExpression": {
177
+ const firstToken = sourceCode.getFirstToken(node);
178
+
179
+ if (!astUtils.isOpeningParenToken(firstToken)) {
180
+
181
+ // If the ArrowFunctionExpression has a single param without parens, return null.
182
+ return null;
183
+ }
184
+
185
+ return {
186
+ leftParen: firstToken,
187
+ rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
188
+ };
189
+ }
190
+
191
+ default:
192
+ throw new TypeError(`unexpected node with type ${node.type}`);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Validates the parentheses for a node
198
+ * @param {ASTNode} node The node with parens
199
+ * @returns {void}
200
+ */
201
+ function validateNode(node) {
202
+ const parens = getParenTokens(node);
203
+
204
+ if (parens) {
205
+ validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments);
206
+ }
207
+ }
208
+
209
+ //----------------------------------------------------------------------
210
+ // Public
211
+ //----------------------------------------------------------------------
212
+
213
+ return {
214
+ ArrowFunctionExpression: validateNode,
215
+ CallExpression: validateNode,
216
+ FunctionDeclaration: validateNode,
217
+ FunctionExpression: validateNode,
218
+ NewExpression: validateNode
219
+ };
220
+ }
221
+ };
@@ -9,6 +9,22 @@
9
9
  // Rule Definition
10
10
  //------------------------------------------------------------------------------
11
11
 
12
+ const OVERRIDE_SCHEMA = {
13
+ oneOf: [
14
+ {
15
+ enum: ["before", "after", "both", "neither"]
16
+ },
17
+ {
18
+ type: "object",
19
+ properties: {
20
+ before: { type: "boolean" },
21
+ after: { type: "boolean" }
22
+ },
23
+ additionalProperties: false
24
+ }
25
+ ]
26
+ };
27
+
12
28
  module.exports = {
13
29
  meta: {
14
30
  docs: {
@@ -29,7 +45,10 @@ module.exports = {
29
45
  type: "object",
30
46
  properties: {
31
47
  before: { type: "boolean" },
32
- after: { type: "boolean" }
48
+ after: { type: "boolean" },
49
+ named: OVERRIDE_SCHEMA,
50
+ anonymous: OVERRIDE_SCHEMA,
51
+ method: OVERRIDE_SCHEMA
33
52
  },
34
53
  additionalProperties: false
35
54
  }
@@ -40,16 +59,39 @@ module.exports = {
40
59
 
41
60
  create(context) {
42
61
 
43
- const mode = (function(option) {
44
- if (!option || typeof option === "string") {
45
- return {
46
- before: { before: true, after: false },
47
- after: { before: false, after: true },
48
- both: { before: true, after: true },
49
- neither: { before: false, after: false }
50
- }[option || "before"];
62
+ const optionDefinitions = {
63
+ before: { before: true, after: false },
64
+ after: { before: false, after: true },
65
+ both: { before: true, after: true },
66
+ neither: { before: false, after: false }
67
+ };
68
+
69
+ /**
70
+ * Returns resolved option definitions based on an option and defaults
71
+ *
72
+ * @param {any} option - The option object or string value
73
+ * @param {Object} defaults - The defaults to use if options are not present
74
+ * @returns {Object} the resolved object definition
75
+ */
76
+ function optionToDefinition(option, defaults) {
77
+ if (!option) {
78
+ return defaults;
51
79
  }
52
- return option;
80
+
81
+ return typeof option === "string"
82
+ ? optionDefinitions[option]
83
+ : Object.assign({}, defaults, option);
84
+ }
85
+
86
+ const modes = (function(option) {
87
+ option = option || {};
88
+ const defaults = optionToDefinition(option, optionDefinitions.before);
89
+
90
+ return {
91
+ named: optionToDefinition(option.named, defaults),
92
+ anonymous: optionToDefinition(option.anonymous, defaults),
93
+ method: optionToDefinition(option.method, defaults)
94
+ };
53
95
  }(context.options[0]));
54
96
 
55
97
  const sourceCode = context.getSourceCode();
@@ -79,6 +121,8 @@ module.exports = {
79
121
 
80
122
  /**
81
123
  * Checks the spacing between two tokens before or after the star token.
124
+ *
125
+ * @param {string} kind Either "named", "anonymous", or "method"
82
126
  * @param {string} side Either "before" or "after".
83
127
  * @param {Token} leftToken `function` keyword token if side is "before", or
84
128
  * star token if side is "after".
@@ -86,10 +130,10 @@ module.exports = {
86
130
  * token if side is "after".
87
131
  * @returns {void}
88
132
  */
89
- function checkSpacing(side, leftToken, rightToken) {
90
- if (!!(rightToken.range[0] - leftToken.range[1]) !== mode[side]) {
133
+ function checkSpacing(kind, side, leftToken, rightToken) {
134
+ if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
91
135
  const after = leftToken.value === "*";
92
- const spaceRequired = mode[side];
136
+ const spaceRequired = modes[kind][side];
93
137
  const node = after ? leftToken : rightToken;
94
138
  const type = spaceRequired ? "Missing" : "Unexpected";
95
139
  const message = "{{type}} space {{side}} *.";
@@ -117,6 +161,7 @@ module.exports = {
117
161
 
118
162
  /**
119
163
  * Enforces the spacing around the star if node is a generator function.
164
+ *
120
165
  * @param {ASTNode} node A function expression or declaration node.
121
166
  * @returns {void}
122
167
  */
@@ -126,17 +171,23 @@ module.exports = {
126
171
  }
127
172
 
128
173
  const starToken = getStarToken(node);
129
-
130
- // Only check before when preceded by `function`|`static` keyword
131
174
  const prevToken = sourceCode.getTokenBefore(starToken);
175
+ const nextToken = sourceCode.getTokenAfter(starToken);
132
176
 
133
- if (prevToken.value === "function" || prevToken.value === "static") {
134
- checkSpacing("before", prevToken, starToken);
177
+ let kind = "named";
178
+
179
+ if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
180
+ kind = "method";
181
+ } else if (!node.id) {
182
+ kind = "anonymous";
135
183
  }
136
184
 
137
- const nextToken = sourceCode.getTokenAfter(starToken);
185
+ // Only check before when preceded by `function`|`static` keyword
186
+ if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
187
+ checkSpacing(kind, "before", prevToken, starToken);
188
+ }
138
189
 
139
- checkSpacing("after", starToken, nextToken);
190
+ checkSpacing(kind, "after", starToken, nextToken);
140
191
  }
141
192
 
142
193
  return {
@@ -965,7 +965,8 @@ module.exports = {
965
965
  const regex = /^return\s*?\(\s*?\);*?/;
966
966
 
967
967
  const statementWithoutArgument = sourceCode.getText(node).replace(
968
- sourceCode.getText(node.argument), "");
968
+ sourceCode.getText(node.argument), ""
969
+ );
969
970
 
970
971
  return regex.test(statementWithoutArgument);
971
972
  }