eslint 4.5.0 → 4.7.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/bin/eslint.js +2 -1
  3. package/conf/eslint-recommended.js +1 -0
  4. package/lib/ast-utils.js +20 -17
  5. package/lib/cli-engine.js +51 -124
  6. package/lib/code-path-analysis/code-path-analyzer.js +8 -4
  7. package/lib/code-path-analysis/code-path-segment.js +2 -1
  8. package/lib/code-path-analysis/code-path-state.js +21 -14
  9. package/lib/code-path-analysis/code-path.js +3 -2
  10. package/lib/code-path-analysis/fork-context.js +2 -1
  11. package/lib/config/autoconfig.js +2 -4
  12. package/lib/config/config-initializer.js +9 -5
  13. package/lib/config/config-ops.js +15 -15
  14. package/lib/config.js +8 -12
  15. package/lib/formatters/codeframe.js +1 -1
  16. package/lib/formatters/stylish.js +1 -1
  17. package/lib/ignored-paths.js +0 -2
  18. package/lib/linter.js +468 -638
  19. package/lib/report-translator.js +274 -0
  20. package/lib/rules/function-paren-newline.js +221 -0
  21. package/lib/rules/generator-star-spacing.js +70 -19
  22. package/lib/rules/indent-legacy.js +3 -2
  23. package/lib/rules/indent.js +15 -6
  24. package/lib/rules/key-spacing.js +2 -1
  25. package/lib/rules/newline-per-chained-call.js +20 -3
  26. package/lib/rules/no-extra-parens.js +75 -33
  27. package/lib/rules/no-invalid-this.js +2 -1
  28. package/lib/rules/no-tabs.js +1 -1
  29. package/lib/rules/no-undef-init.js +4 -0
  30. package/lib/rules/no-unmodified-loop-condition.js +1 -1
  31. package/lib/rules/no-unused-vars.js +47 -4
  32. package/lib/rules/padded-blocks.js +2 -2
  33. package/lib/rules/prefer-arrow-callback.js +1 -2
  34. package/lib/rules/quote-props.js +4 -2
  35. package/lib/rules/quotes.js +1 -2
  36. package/lib/rules/space-before-blocks.js +1 -1
  37. package/lib/rules/valid-jsdoc.js +2 -2
  38. package/lib/rules.js +48 -3
  39. package/lib/testers/rule-tester.js +27 -51
  40. package/lib/timing.js +2 -2
  41. package/lib/util/apply-disable-directives.js +131 -0
  42. package/lib/util/fix-tracker.js +1 -2
  43. package/lib/util/npm-util.js +21 -4
  44. package/lib/util/source-code-fixer.js +5 -14
  45. package/lib/util/source-code.js +3 -5
  46. package/package.json +8 -8
  47. package/lib/rule-context.js +0 -241
  48. package/lib/testers/event-generator-tester.js +0 -62
  49. package/lib/testers/test-parser.js +0 -48
@@ -802,10 +802,19 @@ module.exports = {
802
802
  return;
803
803
  }
804
804
  elements.forEach((element, index) => {
805
+ if (!element) {
806
+
807
+ // Skip holes in arrays
808
+ return;
809
+ }
805
810
  if (offset === "off") {
811
+
812
+ // Ignore the first token of every element if the "off" option is used
806
813
  offsets.ignoreToken(getFirstToken(element));
807
814
  }
808
- if (index === 0 || !element) {
815
+
816
+ // Offset the following elements correctly relative to the first element
817
+ if (index === 0) {
809
818
  return;
810
819
  }
811
820
  if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) {
@@ -941,12 +950,12 @@ module.exports = {
941
950
  }
942
951
 
943
952
  /**
944
- * Check whether the given token is the first token of a statement.
953
+ * Check whether the given token is on the first line of a statement.
945
954
  * @param {Token} token The token to check.
946
955
  * @param {ASTNode} leafNode The expression node that the token belongs directly.
947
- * @returns {boolean} `true` if the token is the first token of a statement.
956
+ * @returns {boolean} `true` if the token is on the first line of a statement.
948
957
  */
949
- function isFirstTokenOfStatement(token, leafNode) {
958
+ function isOnFirstLineOfStatement(token, leafNode) {
950
959
  let node = leafNode;
951
960
 
952
961
  while (node.parent && !node.parent.type.endsWith("Statement") && !node.parent.type.endsWith("Declaration")) {
@@ -954,7 +963,7 @@ module.exports = {
954
963
  }
955
964
  node = node.parent;
956
965
 
957
- return !node || node.range[0] === token.range[0];
966
+ return !node || node.loc.start.line === token.loc.start.line;
958
967
  }
959
968
 
960
969
  const baseOffsetListeners = {
@@ -1067,7 +1076,7 @@ module.exports = {
1067
1076
  // /*else*/ qiz ;
1068
1077
  if (!options.flatTernaryExpressions ||
1069
1078
  !astUtils.isTokenOnSameLine(node.test, node.consequent) ||
1070
- isFirstTokenOfStatement(firstToken, node)
1079
+ isOnFirstLineOfStatement(firstToken, node)
1071
1080
  ) {
1072
1081
  const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?");
1073
1082
  const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":");
@@ -420,7 +420,6 @@ module.exports = {
420
420
  isExtra = diff > 0,
421
421
  diffAbs = Math.abs(diff),
422
422
  spaces = Array(diffAbs + 1).join(" ");
423
- let fix;
424
423
 
425
424
  if ((
426
425
  diff && mode === "strict" ||
@@ -428,6 +427,8 @@ module.exports = {
428
427
  diff > 0 && !expected && mode === "minimum") &&
429
428
  !(expected && containsLineTerminator(whitespace))
430
429
  ) {
430
+ let fix;
431
+
431
432
  if (isExtra) {
432
433
  let range;
433
434
 
@@ -19,7 +19,7 @@ module.exports = {
19
19
  category: "Stylistic Issues",
20
20
  recommended: false
21
21
  },
22
-
22
+ fixable: "whitespace",
23
23
  schema: [{
24
24
  type: "object",
25
25
  properties: {
@@ -40,6 +40,18 @@ module.exports = {
40
40
 
41
41
  const sourceCode = context.getSourceCode();
42
42
 
43
+ /**
44
+ * Get the prefix of a given MemberExpression node.
45
+ * If the MemberExpression node is a computed value it returns a
46
+ * left bracket. If not it returns a period.
47
+ *
48
+ * @param {ASTNode} node - A MemberExpression node to get
49
+ * @returns {string} The prefix of the node.
50
+ */
51
+ function getPrefix(node) {
52
+ return node.computed ? "[" : ".";
53
+ }
54
+
43
55
  /**
44
56
  * Gets the property text of a given MemberExpression node.
45
57
  * If the text is multiline, this returns only the first line.
@@ -48,7 +60,7 @@ module.exports = {
48
60
  * @returns {string} The property text of the node.
49
61
  */
50
62
  function getPropertyText(node) {
51
- const prefix = node.computed ? "[" : ".";
63
+ const prefix = getPrefix(node);
52
64
  const lines = sourceCode.getText(node.property).split(astUtils.LINEBREAK_MATCHER);
53
65
  const suffix = node.computed && lines.length === 1 ? "]" : "";
54
66
 
@@ -70,13 +82,18 @@ module.exports = {
70
82
  parent = parent.callee.object;
71
83
  }
72
84
 
73
- if (depth > ignoreChainWithDepth && callee.property.loc.start.line === callee.object.loc.end.line) {
85
+ if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) {
74
86
  context.report({
75
87
  node: callee.property,
76
88
  loc: callee.property.loc.start,
77
89
  message: "Expected line break before `{{callee}}`.",
78
90
  data: {
79
91
  callee: getPropertyText(callee)
92
+ },
93
+ fix(fixer) {
94
+ const firstTokenAfterObject = sourceCode.getTokenAfter(callee.object, astUtils.isNotClosingParenToken);
95
+
96
+ return fixer.insertTextBefore(firstTokenAfterObject, "\n");
80
97
  }
81
98
  });
82
99
  }
@@ -276,6 +276,15 @@ module.exports = {
276
276
  !astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen);
277
277
  }
278
278
 
279
+ /**
280
+ * Determines if a given expression node is an IIFE
281
+ * @param {ASTNode} node The node to check
282
+ * @returns {boolean} `true` if the given node is an IIFE
283
+ */
284
+ function isIIFE(node) {
285
+ return node.type === "CallExpression" && node.callee.type === "FunctionExpression";
286
+ }
287
+
279
288
  /**
280
289
  * Report the node
281
290
  * @param {ASTNode} node node to evaluate
@@ -286,8 +295,14 @@ module.exports = {
286
295
  const leftParenToken = sourceCode.getTokenBefore(node);
287
296
  const rightParenToken = sourceCode.getTokenAfter(node);
288
297
 
289
- if (tokensToIgnore.has(sourceCode.getFirstToken(node)) && !isParenthesisedTwice(node)) {
290
- return;
298
+ if (!isParenthesisedTwice(node)) {
299
+ if (tokensToIgnore.has(sourceCode.getFirstToken(node))) {
300
+ return;
301
+ }
302
+
303
+ if (isIIFE(node) && !isParenthesised(node.callee)) {
304
+ return;
305
+ }
291
306
  }
292
307
 
293
308
  context.report({
@@ -321,6 +336,23 @@ module.exports = {
321
336
  }
322
337
  }
323
338
 
339
+ /**
340
+ * Check if a member expression contains a call expression
341
+ * @param {ASTNode} node MemberExpression node to evaluate
342
+ * @returns {boolean} true if found, false if not
343
+ */
344
+ function doesMemberExpressionContainCallExpression(node) {
345
+ let currentNode = node.object;
346
+ let currentNodeType = node.object.type;
347
+
348
+ while (currentNodeType === "MemberExpression") {
349
+ currentNode = currentNode.object;
350
+ currentNodeType = currentNode.type;
351
+ }
352
+
353
+ return currentNodeType === "CallExpression";
354
+ }
355
+
324
356
  /**
325
357
  * Evaluate a new call
326
358
  * @param {ASTNode} node node to evaluate
@@ -328,26 +360,32 @@ module.exports = {
328
360
  * @private
329
361
  */
330
362
  function checkCallNew(node) {
331
- if (hasExcessParens(node.callee) && precedence(node.callee) >= precedence(node) && !(
332
- node.type === "CallExpression" &&
333
- (node.callee.type === "FunctionExpression" ||
334
- node.callee.type === "NewExpression" && !isNewExpressionWithParens(node.callee)) &&
335
-
336
- // One set of parentheses are allowed for a function expression
337
- !hasDoubleExcessParens(node.callee)
338
- )) {
339
- report(node.callee);
363
+ const callee = node.callee;
364
+
365
+ if (hasExcessParens(callee) && precedence(callee) >= precedence(node)) {
366
+ const hasNewParensException = callee.type === "NewExpression" && !isNewExpressionWithParens(callee);
367
+
368
+ if (
369
+ hasDoubleExcessParens(callee) ||
370
+ !isIIFE(node) && !hasNewParensException && !(
371
+
372
+ // Allow extra parens around a new expression if
373
+ // there are intervening parentheses.
374
+ callee.type === "MemberExpression" &&
375
+ doesMemberExpressionContainCallExpression(callee)
376
+ )
377
+ ) {
378
+ report(node.callee);
379
+ }
340
380
  }
341
381
  if (node.arguments.length === 1) {
342
382
  if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
343
383
  report(node.arguments[0]);
344
384
  }
345
385
  } else {
346
- [].forEach.call(node.arguments, arg => {
347
- if (hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
348
- report(arg);
349
- }
350
- });
386
+ node.arguments
387
+ .filter(arg => hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
388
+ .forEach(report);
351
389
  }
352
390
  }
353
391
 
@@ -442,11 +480,9 @@ module.exports = {
442
480
 
443
481
  return {
444
482
  ArrayExpression(node) {
445
- [].forEach.call(node.elements, e => {
446
- if (e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
447
- report(e);
448
- }
449
- });
483
+ node.elements
484
+ .filter(e => e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
485
+ .forEach(report);
450
486
  },
451
487
 
452
488
  ArrowFunctionExpression(node) {
@@ -566,8 +602,10 @@ module.exports = {
566
602
  LogicalExpression: checkBinaryLogical,
567
603
 
568
604
  MemberExpression(node) {
605
+ const nodeObjHasExcessParens = hasExcessParens(node.object);
606
+
569
607
  if (
570
- hasExcessParens(node.object) &&
608
+ nodeObjHasExcessParens &&
571
609
  precedence(node.object) >= precedence(node) &&
572
610
  (
573
611
  node.computed ||
@@ -581,6 +619,13 @@ module.exports = {
581
619
  ) {
582
620
  report(node.object);
583
621
  }
622
+
623
+ if (nodeObjHasExcessParens &&
624
+ node.object.type === "CallExpression" &&
625
+ node.parent.type !== "NewExpression") {
626
+ report(node.object);
627
+ }
628
+
584
629
  if (node.computed && hasExcessParens(node.property)) {
585
630
  report(node.property);
586
631
  }
@@ -589,13 +634,12 @@ module.exports = {
589
634
  NewExpression: checkCallNew,
590
635
 
591
636
  ObjectExpression(node) {
592
- [].forEach.call(node.properties, e => {
593
- const v = e.value;
637
+ node.properties
638
+ .filter(property => {
639
+ const value = property.value;
594
640
 
595
- if (v && hasExcessParens(v) && precedence(v) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
596
- report(v);
597
- }
598
- });
641
+ return value && hasExcessParens(value) && precedence(value) >= PRECEDENCE_OF_ASSIGNMENT_EXPR;
642
+ }).forEach(property => report(property.value));
599
643
  },
600
644
 
601
645
  ReturnStatement(node) {
@@ -615,11 +659,9 @@ module.exports = {
615
659
  },
616
660
 
617
661
  SequenceExpression(node) {
618
- [].forEach.call(node.expressions, e => {
619
- if (hasExcessParens(e) && precedence(e) >= precedence(node)) {
620
- report(e);
621
- }
622
- });
662
+ node.expressions
663
+ .filter(e => hasExcessParens(e) && precedence(e) >= precedence(node))
664
+ .forEach(report);
623
665
  },
624
666
 
625
667
  SwitchCase(node) {
@@ -46,7 +46,8 @@ module.exports = {
46
46
  current.init = true;
47
47
  current.valid = !astUtils.isDefaultThisBinding(
48
48
  current.node,
49
- sourceCode);
49
+ sourceCode
50
+ );
50
51
  }
51
52
  return current;
52
53
  };
@@ -27,7 +27,7 @@ module.exports = {
27
27
  create(context) {
28
28
  return {
29
29
  Program(node) {
30
- context.getSourceLines().forEach((line, index) => {
30
+ context.getSourceCode().getLines().forEach((line, index) => {
31
31
  const match = regex.exec(line);
32
32
 
33
33
  if (match) {
@@ -43,6 +43,10 @@ module.exports = {
43
43
  message: "It's not necessary to initialize '{{name}}' to undefined.",
44
44
  data: { name },
45
45
  fix(fixer) {
46
+ if (node.parent.kind === "var") {
47
+ return null;
48
+ }
49
+
46
50
  if (node.id.type === "ArrayPattern" || node.id.type === "ObjectPattern") {
47
51
 
48
52
  // Don't fix destructuring assignment to `undefined`.
@@ -213,13 +213,13 @@ function getEncloseFunctionDeclaration(reference) {
213
213
  * @returns {void}
214
214
  */
215
215
  function updateModifiedFlag(conditions, modifiers) {
216
- let funcNode, funcVar;
217
216
 
218
217
  for (let i = 0; i < conditions.length; ++i) {
219
218
  const condition = conditions[i];
220
219
 
221
220
  for (let j = 0; !condition.modified && j < modifiers.length; ++j) {
222
221
  const modifier = modifiers[j];
222
+ let funcNode, funcVar;
223
223
 
224
224
  /*
225
225
  * Besides checking for the condition being in the loop, we want to
@@ -64,8 +64,6 @@ module.exports = {
64
64
  create(context) {
65
65
  const sourceCode = context.getSourceCode();
66
66
 
67
- const DEFINED_MESSAGE = "'{{name}}' is defined but never used.";
68
- const ASSIGNED_MESSAGE = "'{{name}}' is assigned a value but never used.";
69
67
  const REST_PROPERTY_TYPE = /^(?:Experimental)?RestProperty$/;
70
68
 
71
69
  const config = {
@@ -100,6 +98,49 @@ module.exports = {
100
98
  }
101
99
  }
102
100
 
101
+ /**
102
+ * Generate the warning message about the variable being
103
+ * defined and unused, including the ignore pattern if configured.
104
+ * @param {Variable} unusedVar - eslint-scope variable object.
105
+ * @returns {string} The warning message to be used with this unused variable.
106
+ */
107
+ function getDefinedMessage(unusedVar) {
108
+ let type;
109
+ let pattern;
110
+
111
+ if (config.varsIgnorePattern) {
112
+ type = "vars";
113
+ pattern = config.varsIgnorePattern.toString();
114
+ }
115
+
116
+ if (unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type) {
117
+ const defType = unusedVar.defs[0].type;
118
+
119
+ if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) {
120
+ type = "args";
121
+ pattern = config.caughtErrorsIgnorePattern.toString();
122
+ } else if (defType === "Parameter" && config.argsIgnorePattern) {
123
+ type = "args";
124
+ pattern = config.argsIgnorePattern.toString();
125
+ }
126
+ }
127
+
128
+ const additional = type ? ` Allowed unused ${type} must match ${pattern}.` : "";
129
+
130
+ return `'{{name}}' is defined but never used.${additional}`;
131
+ }
132
+
133
+ /**
134
+ * Generate the warning message about the variable being
135
+ * assigned and unused, including the ignore pattern if configured.
136
+ * @returns {string} The warning message to be used with this unused variable.
137
+ */
138
+ function getAssignedMessage() {
139
+ const additional = config.varsIgnorePattern ? ` Allowed unused vars must match ${config.varsIgnorePattern.toString()}.` : "";
140
+
141
+ return `'{{name}}' is assigned a value but never used.${additional}`;
142
+ }
143
+
103
144
  //--------------------------------------------------------------------------
104
145
  // Helpers
105
146
  //--------------------------------------------------------------------------
@@ -586,13 +627,15 @@ module.exports = {
586
627
  context.report({
587
628
  node: programNode,
588
629
  loc: getLocation(unusedVar),
589
- message: DEFINED_MESSAGE,
630
+ message: getDefinedMessage(unusedVar),
590
631
  data: unusedVar
591
632
  });
592
633
  } else if (unusedVar.defs.length > 0) {
593
634
  context.report({
594
635
  node: unusedVar.identifiers[0],
595
- message: unusedVar.references.some(ref => ref.isWrite()) ? ASSIGNED_MESSAGE : DEFINED_MESSAGE,
636
+ message: unusedVar.references.some(ref => ref.isWrite())
637
+ ? getAssignedMessage()
638
+ : getDefinedMessage(unusedVar),
596
639
  data: unusedVar
597
640
  });
598
641
  }
@@ -111,7 +111,7 @@ module.exports = {
111
111
  * @returns {boolean} Whether or not the token is followed by a blank line.
112
112
  */
113
113
  function getFirstBlockToken(token) {
114
- let prev = token,
114
+ let prev,
115
115
  first = token;
116
116
 
117
117
  do {
@@ -129,7 +129,7 @@ module.exports = {
129
129
  */
130
130
  function getLastBlockToken(token) {
131
131
  let last = token,
132
- next = token;
132
+ next;
133
133
 
134
134
  do {
135
135
  next = last;
@@ -88,7 +88,6 @@ function getCallbackInfo(node) {
88
88
  parent.parent.arguments.length === 1 &&
89
89
  parent.parent.arguments[0].type === "ThisExpression"
90
90
  );
91
- node = parent;
92
91
  parent = parent.parent;
93
92
  } else {
94
93
  return retv;
@@ -133,7 +132,7 @@ function hasDuplicateParams(paramsList) {
133
132
  module.exports = {
134
133
  meta: {
135
134
  docs: {
136
- description: "require arrow functions as callbacks",
135
+ description: "require using arrow functions for callbacks",
137
136
  category: "ECMAScript 6",
138
137
  recommended: false
139
138
  },
@@ -135,13 +135,14 @@ module.exports = {
135
135
  */
136
136
  function checkUnnecessaryQuotes(node) {
137
137
  const key = node.key;
138
- let tokens;
139
138
 
140
139
  if (node.method || node.computed || node.shorthand) {
141
140
  return;
142
141
  }
143
142
 
144
143
  if (key.type === "Literal" && typeof key.value === "string") {
144
+ let tokens;
145
+
145
146
  try {
146
147
  tokens = espree.tokenize(key.value);
147
148
  } catch (e) {
@@ -215,7 +216,6 @@ module.exports = {
215
216
 
216
217
  node.properties.forEach(property => {
217
218
  const key = property.key;
218
- let tokens;
219
219
 
220
220
  if (!key || property.method || property.computed || property.shorthand) {
221
221
  return;
@@ -226,6 +226,8 @@ module.exports = {
226
226
  quotedProps.push(property);
227
227
 
228
228
  if (checkQuotesRedundancy) {
229
+ let tokens;
230
+
229
231
  try {
230
232
  tokens = espree.tokenize(key.value);
231
233
  } catch (e) {
@@ -229,10 +229,9 @@ module.exports = {
229
229
  Literal(node) {
230
230
  const val = node.value,
231
231
  rawVal = node.raw;
232
- let isValid;
233
232
 
234
233
  if (settings && typeof val === "string") {
235
- isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) ||
234
+ let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) ||
236
235
  isJSXLiteral(node) ||
237
236
  astUtils.isSurroundedBy(rawVal, settings.quote);
238
237
 
@@ -82,11 +82,11 @@ module.exports = {
82
82
  */
83
83
  function checkPrecedingSpace(node) {
84
84
  const precedingToken = sourceCode.getTokenBefore(node);
85
- let requireSpace;
86
85
 
87
86
  if (precedingToken && !isConflicted(precedingToken) && astUtils.isTokenOnSameLine(precedingToken, node)) {
88
87
  const hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node);
89
88
  const parent = context.getAncestors().pop();
89
+ let requireSpace;
90
90
 
91
91
  if (parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") {
92
92
  requireSpace = checkFunctions;
@@ -231,11 +231,11 @@ module.exports = {
231
231
  hasConstructor = false,
232
232
  isInterface = false,
233
233
  isOverride = false,
234
- isAbstract = false,
235
- jsdoc;
234
+ isAbstract = false;
236
235
 
237
236
  // make sure only to validate JSDoc comments
238
237
  if (jsdocNode) {
238
+ let jsdoc;
239
239
 
240
240
  try {
241
241
  jsdoc = doctrine.parse(jsdocNode.value, {
package/lib/rules.js CHANGED
@@ -9,7 +9,48 @@
9
9
  // Requirements
10
10
  //------------------------------------------------------------------------------
11
11
 
12
+ const lodash = require("lodash");
12
13
  const loadRules = require("./load-rules");
14
+ const ruleReplacements = require("../conf/replacements").rules;
15
+
16
+ //------------------------------------------------------------------------------
17
+ // Helpers
18
+ //------------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Creates a stub rule that gets used when a rule with a given ID is not found.
22
+ * @param {string} ruleId The ID of the missing rule
23
+ * @returns {{create: function(RuleContext): Object}} A rule that reports an error at the first location
24
+ * in the program. The report has the message `Definition for rule '${ruleId}' was not found` if the rule is unknown,
25
+ * or `Rule '${ruleId}' was removed and replaced by: ${replacements.join(", ")}` if the rule is known to have been
26
+ * replaced.
27
+ */
28
+ const createMissingRule = lodash.memoize(ruleId => {
29
+ const message = Object.prototype.hasOwnProperty.call(ruleReplacements, ruleId)
30
+ ? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements[ruleId].join(", ")}`
31
+ : `Definition for rule '${ruleId}' was not found`;
32
+
33
+ return {
34
+ create: context => ({
35
+ Program() {
36
+ context.report({
37
+ loc: { line: 1, column: 0 },
38
+ message
39
+ });
40
+ }
41
+ })
42
+ };
43
+ });
44
+
45
+ /**
46
+ * Normalizes a rule module to the new-style API
47
+ * @param {(Function|{create: Function})} rule A rule object, which can either be a function
48
+ * ("old-style") or an object with a `create` method ("new-style")
49
+ * @returns {{create: Function}} A new-style rule.
50
+ */
51
+ function normalizeRule(rule) {
52
+ return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule;
53
+ }
13
54
 
14
55
  //------------------------------------------------------------------------------
15
56
  // Public Interface
@@ -29,7 +70,7 @@ class Rules {
29
70
  * @returns {void}
30
71
  */
31
72
  define(ruleId, ruleModule) {
32
- this._rules[ruleId] = ruleModule;
73
+ this._rules[ruleId] = normalizeRule(ruleModule);
33
74
  }
34
75
 
35
76
  /**
@@ -66,11 +107,15 @@ class Rules {
66
107
  /**
67
108
  * Access rule handler by id (file name).
68
109
  * @param {string} ruleId Rule id (file name).
69
- * @returns {Function} Rule handler.
110
+ * @returns {{create: Function, schema: JsonSchema[]}}
111
+ * A rule. This is normalized to always have the new-style shape with a `create` method.
70
112
  */
71
113
  get(ruleId) {
114
+ if (!Object.prototype.hasOwnProperty.call(this._rules, ruleId)) {
115
+ return createMissingRule(ruleId);
116
+ }
72
117
  if (typeof this._rules[ruleId] === "string") {
73
- return require(this._rules[ruleId]);
118
+ return normalizeRule(require(this._rules[ruleId]));
74
119
  }
75
120
  return this._rules[ruleId];
76
121