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.
@@ -581,6 +581,15 @@ module.exports = {
581
581
  ImportDeclaration: ELEMENT_LIST_SCHEMA,
582
582
  flatTernaryExpressions: {
583
583
  type: "boolean"
584
+ },
585
+ ignoredNodes: {
586
+ type: "array",
587
+ items: {
588
+ type: "string",
589
+ not: {
590
+ pattern: ":exit$"
591
+ }
592
+ }
584
593
  }
585
594
  },
586
595
  additionalProperties: false
@@ -618,7 +627,8 @@ module.exports = {
618
627
  ArrayExpression: 1,
619
628
  ObjectExpression: 1,
620
629
  ImportDeclaration: 1,
621
- flatTernaryExpressions: false
630
+ flatTernaryExpressions: false,
631
+ ignoredNodes: []
622
632
  };
623
633
 
624
634
  if (context.options.length) {
@@ -783,7 +793,7 @@ module.exports = {
783
793
  offsets.setDesiredOffsets(
784
794
  [startToken.range[1], endToken.range[0]],
785
795
  startToken,
786
- offset === "first" ? 1 : offset
796
+ typeof offset === "number" ? offset : 1
787
797
  );
788
798
  offsets.setDesiredOffset(endToken, startToken, 0);
789
799
 
@@ -792,10 +802,19 @@ module.exports = {
792
802
  return;
793
803
  }
794
804
  elements.forEach((element, index) => {
805
+ if (!element) {
806
+
807
+ // Skip holes in arrays
808
+ return;
809
+ }
795
810
  if (offset === "off") {
811
+
812
+ // Ignore the first token of every element if the "off" option is used
796
813
  offsets.ignoreToken(getFirstToken(element));
797
814
  }
798
- if (index === 0 || !element) {
815
+
816
+ // Offset the following elements correctly relative to the first element
817
+ if (index === 0) {
799
818
  return;
800
819
  }
801
820
  if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) {
@@ -914,7 +933,7 @@ module.exports = {
914
933
  * @param {ASTNode} node Unknown Node
915
934
  * @returns {void}
916
935
  */
917
- function ignoreUnknownNode(node) {
936
+ function ignoreNode(node) {
918
937
  const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true }));
919
938
 
920
939
  unknownNodeTokens.forEach(token => {
@@ -930,19 +949,6 @@ module.exports = {
930
949
  });
931
950
  }
932
951
 
933
- /**
934
- * Ignore node if it is unknown
935
- * @param {ASTNode} node Node
936
- * @returns {void}
937
- */
938
- function checkForUnknownNode(node) {
939
- const isNodeUnknown = !(KNOWN_NODES.has(node.type));
940
-
941
- if (isNodeUnknown) {
942
- ignoreUnknownNode(node);
943
- }
944
- }
945
-
946
952
  /**
947
953
  * Check whether the given token is the first token of a statement.
948
954
  * @param {Token} token The token to check.
@@ -960,7 +966,7 @@ module.exports = {
960
966
  return !node || node.range[0] === token.range[0];
961
967
  }
962
968
 
963
- return {
969
+ const baseOffsetListeners = {
964
970
  "ArrayExpression, ArrayPattern"(node) {
965
971
  const openingBracket = sourceCode.getFirstToken(node);
966
972
  const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
@@ -1178,14 +1184,15 @@ module.exports = {
1178
1184
  }
1179
1185
  },
1180
1186
 
1181
- "MemberExpression, JSXMemberExpression"(node) {
1182
- const firstNonObjectToken = sourceCode.getFirstTokenBetween(node.object, node.property, astUtils.isNotClosingParenToken);
1187
+ "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
1188
+ const object = node.type === "MetaProperty" ? node.meta : node.object;
1189
+ const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
1183
1190
  const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
1184
1191
 
1185
- const objectParenCount = sourceCode.getTokensBetween(node.object, node.property, { filter: astUtils.isClosingParenToken }).length;
1192
+ const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
1186
1193
  const firstObjectToken = objectParenCount
1187
- ? sourceCode.getTokenBefore(node.object, { skip: objectParenCount - 1 })
1188
- : sourceCode.getFirstToken(node.object);
1194
+ ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
1195
+ : sourceCode.getFirstToken(object);
1189
1196
  const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
1190
1197
  const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
1191
1198
 
@@ -1333,8 +1340,6 @@ module.exports = {
1333
1340
  }
1334
1341
  },
1335
1342
 
1336
- "*:exit": checkForUnknownNode,
1337
-
1338
1343
  "JSXAttribute[value]"(node) {
1339
1344
  const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
1340
1345
 
@@ -1378,62 +1383,130 @@ module.exports = {
1378
1383
  1
1379
1384
  );
1380
1385
  offsets.setDesiredOffset(closingCurly, openingCurly, 0);
1381
- },
1386
+ }
1387
+ };
1382
1388
 
1383
- "Program:exit"() {
1384
- addParensIndent(sourceCode.ast.tokens);
1389
+ const listenerCallQueue = [];
1385
1390
 
1386
- /*
1387
- * Create a Map from (tokenOrComment) => (precedingToken).
1388
- * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1389
- */
1390
- const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1391
- const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1391
+ /*
1392
+ * To ignore the indentation of a node:
1393
+ * 1. Don't call the node's listener when entering it (if it has a listener)
1394
+ * 2. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
1395
+ */
1396
+ const offsetListeners = lodash.mapValues(
1397
+ baseOffsetListeners,
1398
+
1399
+ /*
1400
+ * Offset listener calls are deferred until traversal is finished, and are called as
1401
+ * part of the final `Program:exit` listener. This is necessary because a node might
1402
+ * be matched by multiple selectors.
1403
+ *
1404
+ * Example: Suppose there is an offset listener for `Identifier`, and the user has
1405
+ * specified in configuration that `MemberExpression > Identifier` should be ignored.
1406
+ * Due to selector specificity rules, the `Identifier` listener will get called first. However,
1407
+ * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
1408
+ * should not have been called at all. Without doing extra selector matching, we don't know
1409
+ * whether the Identifier matches the `MemberExpression > Identifier` selector until the
1410
+ * `MemberExpression > Identifier` listener is called.
1411
+ *
1412
+ * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
1413
+ * ignored nodes are known.
1414
+ */
1415
+ listener =>
1416
+ node =>
1417
+ listenerCallQueue.push({ listener, node })
1418
+ );
1392
1419
 
1393
- return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1394
- }, new WeakMap());
1420
+ // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
1421
+ const ignoredNodes = new Set();
1422
+ const addToIgnoredNodes = ignoredNodes.add.bind(ignoredNodes);
1395
1423
 
1396
- sourceCode.lines.forEach((line, lineIndex) => {
1397
- const lineNumber = lineIndex + 1;
1424
+ const ignoredNodeListeners = options.ignoredNodes.reduce(
1425
+ (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
1426
+ {}
1427
+ );
1398
1428
 
1399
- if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1429
+ /*
1430
+ * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
1431
+ * at the end.
1432
+ *
1433
+ * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
1434
+ * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
1435
+ * so those listeners wouldn't be called anyway.
1436
+ */
1437
+ return Object.assign(
1438
+ offsetListeners,
1439
+ ignoredNodeListeners,
1440
+ {
1441
+ "*:exit"(node) {
1400
1442
 
1401
- // Don't check indentation on blank lines
1402
- return;
1443
+ // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
1444
+ if (!KNOWN_NODES.has(node.type)) {
1445
+ ignoredNodes.add(node);
1403
1446
  }
1447
+ },
1448
+ "Program:exit"() {
1404
1449
 
1405
- const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1450
+ // Invoke the queued offset listeners for the nodes that aren't ignored.
1451
+ listenerCallQueue
1452
+ .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node))
1453
+ .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node));
1406
1454
 
1407
- if (firstTokenOfLine.loc.start.line !== lineNumber) {
1455
+ // Update the offsets for ignored nodes to prevent their child tokens from being reported.
1456
+ ignoredNodes.forEach(ignoreNode);
1408
1457
 
1409
- // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1410
- return;
1411
- }
1458
+ addParensIndent(sourceCode.ast.tokens);
1412
1459
 
1413
- // If the token matches the expected expected indentation, don't report it.
1414
- if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1415
- return;
1416
- }
1460
+ /*
1461
+ * Create a Map from (tokenOrComment) => (precedingToken).
1462
+ * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1463
+ */
1464
+ const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1465
+ const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1466
+
1467
+ return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1468
+ }, new WeakMap());
1417
1469
 
1418
- if (astUtils.isCommentToken(firstTokenOfLine)) {
1419
- const tokenBefore = precedingTokens.get(firstTokenOfLine);
1420
- const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
1470
+ sourceCode.lines.forEach((line, lineIndex) => {
1471
+ const lineNumber = lineIndex + 1;
1421
1472
 
1422
- // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1423
- if (
1424
- tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1425
- tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1426
- ) {
1473
+ if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1474
+
1475
+ // Don't check indentation on blank lines
1427
1476
  return;
1428
1477
  }
1429
- }
1430
1478
 
1431
- // Otherwise, report the token/comment.
1432
- report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1433
- });
1434
- }
1479
+ const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1435
1480
 
1436
- };
1481
+ if (firstTokenOfLine.loc.start.line !== lineNumber) {
1437
1482
 
1483
+ // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1484
+ return;
1485
+ }
1486
+
1487
+ // If the token matches the expected expected indentation, don't report it.
1488
+ if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1489
+ return;
1490
+ }
1491
+
1492
+ if (astUtils.isCommentToken(firstTokenOfLine)) {
1493
+ const tokenBefore = precedingTokens.get(firstTokenOfLine);
1494
+ const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
1495
+
1496
+ // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1497
+ if (
1498
+ tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1499
+ tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1500
+ ) {
1501
+ return;
1502
+ }
1503
+ }
1504
+
1505
+ // Otherwise, report the token/comment.
1506
+ report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1507
+ });
1508
+ }
1509
+ }
1510
+ );
1438
1511
  }
1439
1512
  };
@@ -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({
@@ -328,26 +343,21 @@ module.exports = {
328
343
  * @private
329
344
  */
330
345
  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);
346
+ if (hasExcessParens(node.callee) && precedence(node.callee) >= precedence(node)) {
347
+ const hasNewParensException = node.callee.type === "NewExpression" && !isNewExpressionWithParens(node.callee);
348
+
349
+ if (hasDoubleExcessParens(node.callee) || !isIIFE(node) && !hasNewParensException) {
350
+ report(node.callee);
351
+ }
340
352
  }
341
353
  if (node.arguments.length === 1) {
342
354
  if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
343
355
  report(node.arguments[0]);
344
356
  }
345
357
  } else {
346
- [].forEach.call(node.arguments, arg => {
347
- if (hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
348
- report(arg);
349
- }
350
- });
358
+ node.arguments
359
+ .filter(arg => hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
360
+ .forEach(report);
351
361
  }
352
362
  }
353
363
 
@@ -442,11 +452,9 @@ module.exports = {
442
452
 
443
453
  return {
444
454
  ArrayExpression(node) {
445
- [].forEach.call(node.elements, e => {
446
- if (e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
447
- report(e);
448
- }
449
- });
455
+ node.elements
456
+ .filter(e => e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
457
+ .forEach(report);
450
458
  },
451
459
 
452
460
  ArrowFunctionExpression(node) {
@@ -589,13 +597,12 @@ module.exports = {
589
597
  NewExpression: checkCallNew,
590
598
 
591
599
  ObjectExpression(node) {
592
- [].forEach.call(node.properties, e => {
593
- const v = e.value;
600
+ node.properties
601
+ .filter(property => {
602
+ const value = property.value;
594
603
 
595
- if (v && hasExcessParens(v) && precedence(v) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
596
- report(v);
597
- }
598
- });
604
+ return value && hasExcessParens(value) && precedence(value) >= PRECEDENCE_OF_ASSIGNMENT_EXPR;
605
+ }).forEach(property => report(property.value));
599
606
  },
600
607
 
601
608
  ReturnStatement(node) {
@@ -615,11 +622,9 @@ module.exports = {
615
622
  },
616
623
 
617
624
  SequenceExpression(node) {
618
- [].forEach.call(node.expressions, e => {
619
- if (hasExcessParens(e) && precedence(e) >= precedence(node)) {
620
- report(e);
621
- }
622
- });
625
+ node.expressions
626
+ .filter(e => hasExcessParens(e) && precedence(e) >= precedence(node))
627
+ .forEach(report);
623
628
  },
624
629
 
625
630
  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
  };
@@ -76,8 +76,11 @@ module.exports = {
76
76
  }
77
77
  const rightToken = tokensAndComments[leftIndex + 1];
78
78
 
79
- // Ignore tokens that have less than 2 spaces between them or are on different lines
80
- if (leftToken.range[1] + 2 > rightToken.range[0] || leftToken.loc.end.line < rightToken.loc.start.line) {
79
+ // Ignore tokens that don't have 2 spaces between them or are on different lines
80
+ if (
81
+ !sourceCode.text.slice(leftToken.range[1], rightToken.range[0]).includes(" ") ||
82
+ leftToken.loc.end.line < rightToken.loc.start.line
83
+ ) {
81
84
  return;
82
85
  }
83
86
 
@@ -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
  },
@@ -159,7 +159,8 @@ class RuleTester {
159
159
 
160
160
  // we have to clone because merge uses the first argument for recipient
161
161
  lodash.cloneDeep(defaultConfig),
162
- testerConfig
162
+ testerConfig,
163
+ { rules: { "rule-tester/validate-ast": "error" } }
163
164
  );
164
165
 
165
166
  /**
@@ -333,13 +334,14 @@ class RuleTester {
333
334
  */
334
335
  linter.reset();
335
336
 
336
- linter.on("Program", node => {
337
- beforeAST = cloneDeeplyExcludesParent(node);
338
- });
339
-
340
- linter.on("Program:exit", node => {
341
- afterAST = node;
342
- });
337
+ linter.defineRule("rule-tester/validate-ast", () => ({
338
+ Program(node) {
339
+ beforeAST = cloneDeeplyExcludesParent(node);
340
+ },
341
+ "Program:exit"(node) {
342
+ afterAST = node;
343
+ }
344
+ }));
343
345
 
344
346
  // Freezes rule-context properties.
345
347
  const originalGet = linter.rules.get;
@@ -519,7 +521,7 @@ class RuleTester {
519
521
  "Expected no autofixes to be suggested"
520
522
  );
521
523
  } else {
522
- const fixResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
524
+ const fixResult = SourceCodeFixer.applyFixes(item.code, messages);
523
525
 
524
526
  assert.equal(fixResult.output, item.output, "Output is incorrect.");
525
527
  }
package/lib/timing.js CHANGED
@@ -84,11 +84,11 @@ function display(data) {
84
84
  }
85
85
  });
86
86
 
87
- const table = rows.map(row =>
87
+ const table = rows.map(row => (
88
88
  row
89
89
  .map((cell, index) => ALIGN[index](cell, widths[index]))
90
90
  .join(" | ")
91
- );
91
+ ));
92
92
 
93
93
  table.splice(1, 0, widths.map((w, index) => {
94
94
  if (index !== 0 && index !== widths.length - 1) {
@@ -57,8 +57,7 @@ class FixTracker {
57
57
  retainEnclosingFunction(node) {
58
58
  const functionNode = astUtils.getUpperFunction(node);
59
59
 
60
- return this.retainRange(
61
- functionNode ? functionNode.range : this.sourceCode.ast.range);
60
+ return this.retainRange(functionNode ? functionNode.range : this.sourceCode.ast.range);
62
61
  }
63
62
 
64
63
  /**
@@ -53,22 +53,39 @@ function installSyncSaveDev(packages) {
53
53
  if (!Array.isArray(packages)) {
54
54
  packages = [packages];
55
55
  }
56
- spawn.sync("npm", ["i", "--save-dev"].concat(packages), { stdio: "inherit" });
56
+ const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packages),
57
+ { stdio: "inherit" });
58
+ const error = npmProcess.error;
59
+
60
+ if (error && error.code === "ENOENT") {
61
+ const pluralS = packages.length > 1 ? "s" : "";
62
+
63
+ log.error(`Could not execute npm. Please install the following package${pluralS} with your package manager of choice: ${packages.join(", ")}`);
64
+ }
57
65
  }
58
66
 
59
67
  /**
60
68
  * Fetch `peerDependencies` of the given package by `npm show` command.
61
69
  * @param {string} packageName The package name to fetch peerDependencies.
62
- * @returns {Object} Gotten peerDependencies.
70
+ * @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
63
71
  */
64
72
  function fetchPeerDependencies(packageName) {
65
- const fetchedText = spawn.sync(
73
+ const npmProcess = spawn.sync(
66
74
  "npm",
67
75
  ["show", "--json", packageName, "peerDependencies"],
68
76
  { encoding: "utf8" }
69
- ).stdout.trim();
77
+ );
78
+
79
+ const error = npmProcess.error;
80
+
81
+ if (error && error.code === "ENOENT") {
82
+ return null;
83
+ }
84
+ const fetchedText = npmProcess.stdout.trim();
70
85
 
71
86
  return JSON.parse(fetchedText || "{}");
87
+
88
+
72
89
  }
73
90
 
74
91
  /**