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.
package/lib/linter.js CHANGED
@@ -9,10 +9,10 @@
9
9
  // Requirements
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- const assert = require("assert"),
13
- EventEmitter = require("events").EventEmitter,
12
+ const EventEmitter = require("events").EventEmitter,
14
13
  eslintScope = require("eslint-scope"),
15
14
  levn = require("levn"),
15
+ lodash = require("lodash"),
16
16
  blankScriptAST = require("../conf/blank-script.json"),
17
17
  defaultConfig = require("../conf/default-config-options.js"),
18
18
  replacements = require("../conf/replacements.json"),
@@ -23,7 +23,7 @@ const assert = require("assert"),
23
23
  NodeEventGenerator = require("./util/node-event-generator"),
24
24
  SourceCode = require("./util/source-code"),
25
25
  Traverser = require("./util/traverser"),
26
- RuleContext = require("./rule-context"),
26
+ createReportTranslator = require("./report-translator"),
27
27
  Rules = require("./rules"),
28
28
  timing = require("./timing"),
29
29
  astUtils = require("./ast-utils"),
@@ -87,10 +87,9 @@ function parseBooleanConfig(string, comment) {
87
87
  * Parses a JSON-like config.
88
88
  * @param {string} string The string to parse.
89
89
  * @param {Object} location Start line and column of comments for potential error message.
90
- * @param {Object[]} messages The messages queue for potential error message.
91
- * @returns {Object} Result map object
90
+ * @returns {({success: true, config: Object}|{success: false, error: Problem})} Result map object
92
91
  */
93
- function parseJsonConfig(string, location, messages) {
92
+ function parseJsonConfig(string, location) {
94
93
  let items = {};
95
94
 
96
95
  // Parses a JSON-like comment by the same way as parsing CLI option.
@@ -102,7 +101,10 @@ function parseJsonConfig(string, location, messages) {
102
101
  // "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
103
102
  // Should ignore that case as well.
104
103
  if (ConfigOps.isEverySeverityValid(items)) {
105
- return items;
104
+ return {
105
+ success: true,
106
+ config: items
107
+ };
106
108
  }
107
109
  } catch (ex) {
108
110
 
@@ -116,20 +118,25 @@ function parseJsonConfig(string, location, messages) {
116
118
  try {
117
119
  items = JSON.parse(`{${string}}`);
118
120
  } catch (ex) {
119
-
120
- messages.push({
121
- ruleId: null,
122
- fatal: true,
123
- severity: 2,
124
- source: null,
125
- message: `Failed to parse JSON from '${string}': ${ex.message}`,
126
- line: location.start.line,
127
- column: location.start.column + 1
128
- });
121
+ return {
122
+ success: false,
123
+ error: {
124
+ ruleId: null,
125
+ fatal: true,
126
+ severity: 2,
127
+ source: null,
128
+ message: `Failed to parse JSON from '${string}': ${ex.message}`,
129
+ line: location.start.line,
130
+ column: location.start.column + 1
131
+ }
132
+ };
129
133
 
130
134
  }
131
135
 
132
- return items;
136
+ return {
137
+ success: true,
138
+ config: items
139
+ };
133
140
  }
134
141
 
135
142
  /**
@@ -171,14 +178,12 @@ function addDeclaredGlobals(program, globalScope, config, envContext) {
171
178
 
172
179
  Object.assign(declaredGlobals, builtin);
173
180
 
174
- Object.keys(config.env).forEach(name => {
175
- if (config.env[name]) {
176
- const env = envContext.get(name),
177
- environmentGlobals = env && env.globals;
181
+ Object.keys(config.env).filter(name => config.env[name]).forEach(name => {
182
+ const env = envContext.get(name),
183
+ environmentGlobals = env && env.globals;
178
184
 
179
- if (environmentGlobals) {
180
- Object.assign(declaredGlobals, environmentGlobals);
181
- }
185
+ if (environmentGlobals) {
186
+ Object.assign(declaredGlobals, environmentGlobals);
182
187
  }
183
188
  });
184
189
 
@@ -318,7 +323,8 @@ function enableReporting(reportingConfig, start, rulesToEnable) {
318
323
  * @param {ASTNode} ast The top node of the AST.
319
324
  * @param {Object} config The existing configuration data.
320
325
  * @param {Linter} linterContext Linter context object
321
- * @returns {Object} Modified config object
326
+ * @returns {{config: Object, problems: Problem[]}} Modified config object, along with any problems encountered
327
+ * while parsing config comments
322
328
  */
323
329
  function modifyConfigsFromComments(filename, ast, config, linterContext) {
324
330
 
@@ -329,7 +335,7 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
329
335
  env: {}
330
336
  };
331
337
  const commentRules = {};
332
- const messages = linterContext.messages;
338
+ const problems = [];
333
339
  const reportingConfig = linterContext.reportingConfig;
334
340
 
335
341
  ast.comments.forEach(comment => {
@@ -364,14 +370,19 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
364
370
  break;
365
371
 
366
372
  case "eslint": {
367
- const items = parseJsonConfig(value, comment.loc, messages);
373
+ const parseResult = parseJsonConfig(value, comment.loc);
368
374
 
369
- Object.keys(items).forEach(name => {
370
- const ruleValue = items[name];
375
+ if (parseResult.success) {
376
+ Object.keys(parseResult.config).forEach(name => {
377
+ const ruleValue = parseResult.config[name];
378
+
379
+ validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
380
+ commentRules[name] = ruleValue;
381
+ });
382
+ } else {
383
+ problems.push(parseResult.error);
384
+ }
371
385
 
372
- validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
373
- commentRules[name] = ruleValue;
374
- });
375
386
  break;
376
387
  }
377
388
 
@@ -399,14 +410,17 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
399
410
  });
400
411
  Object.assign(commentConfig.rules, commentRules);
401
412
 
402
- return ConfigOps.merge(config, commentConfig);
413
+ return {
414
+ config: ConfigOps.merge(config, commentConfig),
415
+ problems
416
+ };
403
417
  }
404
418
 
405
419
  /**
406
420
  * Check if message of rule with ruleId should be ignored in location
407
421
  * @param {Object[]} reportingConfig Collection of ignore records
408
422
  * @param {string} ruleId Id of rule
409
- * @param {Object} location Location of message
423
+ * @param {Object} location 1-indexed location of message
410
424
  * @returns {boolean} True if message should be ignored, false otherwise
411
425
  */
412
426
  function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
@@ -416,8 +430,8 @@ function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
416
430
  const ignore = reportingConfig[i];
417
431
 
418
432
  if ((!ignore.rule || ignore.rule === ruleId) &&
419
- (location.line > ignore.start.line || (location.line === ignore.start.line && location.column >= ignore.start.column)) &&
420
- (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column <= ignore.end.column)))) {
433
+ (location.line > ignore.start.line || (location.line === ignore.start.line && location.column > ignore.start.column)) &&
434
+ (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column - 1 <= ignore.end.column)))) {
421
435
  return true;
422
436
  }
423
437
  }
@@ -531,6 +545,8 @@ function createStubRule(message) {
531
545
  if (message) {
532
546
  return createRuleModule;
533
547
  }
548
+
549
+ /* istanbul ignore next */
534
550
  throw new Error("No message passed to stub rule");
535
551
 
536
552
  }
@@ -595,13 +611,7 @@ function stripUnicodeBOM(text) {
595
611
  * @returns {number} 0, 1, or 2, indicating rule severity
596
612
  */
597
613
  function getRuleSeverity(ruleConfig) {
598
- if (typeof ruleConfig === "number") {
599
- return ruleConfig;
600
- } else if (Array.isArray(ruleConfig)) {
601
- return ruleConfig[0];
602
- }
603
- return 0;
604
-
614
+ return Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
605
615
  }
606
616
 
607
617
  /**
@@ -622,45 +632,41 @@ function getRuleOptions(ruleConfig) {
622
632
  * optimization of functions, so it's best to keep the try-catch as isolated
623
633
  * as possible
624
634
  * @param {string} text The text to parse.
625
- * @param {Object} config The ESLint configuration object.
635
+ * @param {Object} providedParserOptions Options to pass to the parser
636
+ * @param {string} parserName The name of the parser
626
637
  * @param {string} filePath The path to the file being parsed.
627
- * @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
628
- * or null if not.
629
- * @param {Array<Object>} messages Messages array for the linter object
630
- * @returns {*} parsed text if successful otherwise null
638
+ * @returns {{success: false, error: Problem}|{success: true,ast: ASTNode, services: Object}}
639
+ * An object containing the AST and parser services if parsing was successful, or the error if parsing failed
631
640
  * @private
632
641
  */
633
- function parse(text, config, filePath, messages) {
634
-
635
- let parser,
636
- parserOptions = {
637
- loc: true,
638
- range: true,
639
- raw: true,
640
- tokens: true,
641
- comment: true,
642
- filePath
643
- };
642
+ function parse(text, providedParserOptions, parserName, filePath) {
643
+
644
+ const parserOptions = Object.assign({}, providedParserOptions, {
645
+ loc: true,
646
+ range: true,
647
+ raw: true,
648
+ tokens: true,
649
+ comment: true,
650
+ filePath
651
+ });
652
+
653
+ let parser;
644
654
 
645
655
  try {
646
- parser = require(config.parser);
656
+ parser = require(parserName);
647
657
  } catch (ex) {
648
- messages.push({
649
- ruleId: null,
650
- fatal: true,
651
- severity: 2,
652
- source: null,
653
- message: ex.message,
654
- line: 0,
655
- column: 0
656
- });
657
-
658
- return null;
659
- }
660
-
661
- // merge in any additional parser options
662
- if (config.parserOptions) {
663
- parserOptions = Object.assign({}, config.parserOptions, parserOptions);
658
+ return {
659
+ success: false,
660
+ error: {
661
+ ruleId: null,
662
+ fatal: true,
663
+ severity: 2,
664
+ source: null,
665
+ message: ex.message,
666
+ line: 0,
667
+ column: 0
668
+ }
669
+ };
664
670
  }
665
671
 
666
672
  /*
@@ -671,31 +677,79 @@ function parse(text, config, filePath, messages) {
671
677
  */
672
678
  try {
673
679
  if (typeof parser.parseForESLint === "function") {
674
- return parser.parseForESLint(text, parserOptions);
680
+ const parseResult = parser.parseForESLint(text, parserOptions);
681
+
682
+ return {
683
+ success: true,
684
+ ast: parseResult.ast,
685
+ services: parseResult.services || {}
686
+ };
675
687
  }
676
- return parser.parse(text, parserOptions);
677
688
 
689
+ return {
690
+ success: true,
691
+ ast: parser.parse(text, parserOptions),
692
+ services: {}
693
+ };
678
694
  } catch (ex) {
679
695
 
680
696
  // If the message includes a leading line number, strip it:
681
- const message = ex.message.replace(/^line \d+:/i, "").trim();
682
- const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
683
-
684
- messages.push({
685
- ruleId: null,
686
- fatal: true,
687
- severity: 2,
688
- source,
689
- message: `Parsing error: ${message}`,
690
-
691
- line: ex.lineNumber,
692
- column: ex.column
693
- });
697
+ const message = `Parsing error: ${ex.message.replace(/^line \d+:/i, "").trim()}`;
698
+ const source = ex.lineNumber ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
694
699
 
695
- return null;
700
+ return {
701
+ success: false,
702
+ error: {
703
+ ruleId: null,
704
+ fatal: true,
705
+ severity: 2,
706
+ source,
707
+ message,
708
+ line: ex.lineNumber,
709
+ column: ex.column
710
+ }
711
+ };
696
712
  }
697
713
  }
698
714
 
715
+ // methods that exist on SourceCode object
716
+ const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
717
+ getSource: "getText",
718
+ getSourceLines: "getLines",
719
+ getAllComments: "getAllComments",
720
+ getNodeByRangeIndex: "getNodeByRangeIndex",
721
+ getComments: "getComments",
722
+ getCommentsBefore: "getCommentsBefore",
723
+ getCommentsAfter: "getCommentsAfter",
724
+ getCommentsInside: "getCommentsInside",
725
+ getJSDocComment: "getJSDocComment",
726
+ getFirstToken: "getFirstToken",
727
+ getFirstTokens: "getFirstTokens",
728
+ getLastToken: "getLastToken",
729
+ getLastTokens: "getLastTokens",
730
+ getTokenAfter: "getTokenAfter",
731
+ getTokenBefore: "getTokenBefore",
732
+ getTokenByRangeStart: "getTokenByRangeStart",
733
+ getTokens: "getTokens",
734
+ getTokensAfter: "getTokensAfter",
735
+ getTokensBefore: "getTokensBefore",
736
+ getTokensBetween: "getTokensBetween"
737
+ };
738
+
739
+ const BASE_TRAVERSAL_CONTEXT = Object.freeze(
740
+ Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
741
+ (contextInfo, methodName) =>
742
+ Object.assign(contextInfo, {
743
+ [methodName]() {
744
+ const sourceCode = this.getSourceCode();
745
+
746
+ return sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]].apply(sourceCode, arguments);
747
+ }
748
+ }),
749
+ {}
750
+ )
751
+ );
752
+
699
753
  //------------------------------------------------------------------------------
700
754
  // Public Interface
701
755
  //------------------------------------------------------------------------------
@@ -704,13 +758,11 @@ function parse(text, config, filePath, messages) {
704
758
  * Object that is responsible for verifying JavaScript text
705
759
  * @name eslint
706
760
  */
707
- class Linter extends EventEmitter {
761
+ class Linter {
708
762
 
709
763
  constructor() {
710
- super();
711
764
  this.messages = [];
712
765
  this.currentConfig = null;
713
- this.currentScopes = null;
714
766
  this.scopeManager = null;
715
767
  this.currentFilename = null;
716
768
  this.traverser = null;
@@ -720,9 +772,6 @@ class Linter extends EventEmitter {
720
772
 
721
773
  this.rules = new Rules();
722
774
  this.environments = new Environments();
723
-
724
- // set unlimited listeners (see https://github.com/eslint/eslint/issues/524)
725
- this.setMaxListeners(0);
726
775
  }
727
776
 
728
777
  /**
@@ -730,10 +779,8 @@ class Linter extends EventEmitter {
730
779
  * @returns {void}
731
780
  */
732
781
  reset() {
733
- this.removeAllListeners();
734
782
  this.messages = [];
735
783
  this.currentConfig = null;
736
- this.currentScopes = null;
737
784
  this.scopeManager = null;
738
785
  this.traverser = null;
739
786
  this.reportingConfig = [];
@@ -765,11 +812,18 @@ class Linter extends EventEmitter {
765
812
  * @returns {Object[]} The results as an array of messages or null if no messages.
766
813
  */
767
814
  verify(textOrSourceCode, config, filenameOrOptions, saveState) {
768
- const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null;
769
- let ast,
770
- parseResult,
815
+ let text,
816
+ parserServices,
771
817
  allowInlineConfig;
772
818
 
819
+ if (typeof textOrSourceCode === "string") {
820
+ this.sourceCode = null;
821
+ text = textOrSourceCode;
822
+ } else {
823
+ this.sourceCode = textOrSourceCode;
824
+ text = this.sourceCode.text;
825
+ }
826
+
773
827
  // evaluate arguments
774
828
  if (typeof filenameOrOptions === "object") {
775
829
  this.currentFilename = filenameOrOptions.filename;
@@ -784,7 +838,7 @@ class Linter extends EventEmitter {
784
838
  }
785
839
 
786
840
  // search and apply "eslint-env *".
787
- const envInFile = findEslintEnv(text || textOrSourceCode.text);
841
+ const envInFile = findEslintEnv(text);
788
842
 
789
843
  config = Object.assign({}, config);
790
844
 
@@ -799,136 +853,191 @@ class Linter extends EventEmitter {
799
853
  // process initial config to make it safe to extend
800
854
  config = prepareConfig(config, this.environments);
801
855
 
802
- // only do this for text
803
- if (text !== null) {
856
+ if (this.sourceCode) {
857
+ parserServices = {};
858
+ } else {
804
859
 
805
860
  // there's no input, just exit here
806
861
  if (text.trim().length === 0) {
807
862
  this.sourceCode = new SourceCode(text, blankScriptAST);
808
- return this.messages;
863
+ return [];
809
864
  }
810
865
 
811
- parseResult = parse(
866
+ const parseResult = parse(
812
867
  stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`),
813
- config,
814
- this.currentFilename,
815
- this.messages
868
+ config.parserOptions,
869
+ config.parser,
870
+ this.currentFilename
816
871
  );
817
872
 
818
- // if this result is from a parseForESLint() method, normalize
819
- if (parseResult && parseResult.ast) {
820
- ast = parseResult.ast;
821
- } else {
822
- ast = parseResult;
823
- parseResult = null;
824
- }
825
-
826
- if (ast) {
827
- this.sourceCode = new SourceCode(text, ast);
873
+ if (!parseResult.success) {
874
+ return [parseResult.error];
828
875
  }
829
876
 
830
- } else {
831
- this.sourceCode = textOrSourceCode;
832
- ast = this.sourceCode.ast;
877
+ parserServices = parseResult.services;
878
+ this.sourceCode = new SourceCode(text, parseResult.ast);
833
879
  }
834
880
 
835
- // if espree failed to parse the file, there's no sense in setting up rules
836
- if (ast) {
881
+ // parse global comments and modify config
882
+ if (allowInlineConfig !== false) {
883
+ const modifyConfigResult = modifyConfigsFromComments(this.currentFilename, this.sourceCode.ast, config, this);
837
884
 
838
- // parse global comments and modify config
839
- if (allowInlineConfig !== false) {
840
- config = modifyConfigsFromComments(this.currentFilename, ast, config, this);
841
- }
842
-
843
- // ensure that severities are normalized in the config
844
- ConfigOps.normalize(config);
845
-
846
- // enable appropriate rules
847
- Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
848
- let ruleCreator;
885
+ config = modifyConfigResult.config;
886
+ modifyConfigResult.problems.forEach(problem => this.messages.push(problem));
887
+ }
849
888
 
850
- ruleCreator = this.rules.get(key);
889
+ // ensure that severities are normalized in the config
890
+ ConfigOps.normalize(config);
851
891
 
852
- if (!ruleCreator) {
853
- const replacementMsg = getRuleReplacementMessage(key);
892
+ const emitter = new EventEmitter().setMaxListeners(Infinity);
854
893
 
855
- if (replacementMsg) {
856
- ruleCreator = createStubRule(replacementMsg);
857
- } else {
858
- ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
894
+ /*
895
+ * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
896
+ * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
897
+ * properties once for each rule.
898
+ */
899
+ const sharedTraversalContext = Object.freeze(
900
+ Object.assign(
901
+ Object.create(BASE_TRAVERSAL_CONTEXT),
902
+ {
903
+ getAncestors: this.getAncestors.bind(this),
904
+ getDeclaredVariables: this.getDeclaredVariables.bind(this),
905
+ getFilename: this.getFilename.bind(this),
906
+ getScope: this.getScope.bind(this),
907
+ getSourceCode: () => this.sourceCode,
908
+ markVariableAsUsed: this.markVariableAsUsed.bind(this),
909
+ parserOptions: config.parserOptions,
910
+ parserPath: config.parser,
911
+ parserServices,
912
+ settings: config.settings,
913
+
914
+ /**
915
+ * This is used to avoid breaking rules that used to monkeypatch the `Linter#report` method
916
+ * by using the `_linter` property on rule contexts.
917
+ *
918
+ * This should be removed in a major release after we create a better way to
919
+ * lint for unused disable comments.
920
+ * https://github.com/eslint/eslint/issues/9193
921
+ */
922
+ _linter: {
923
+ report() {},
924
+ on: emitter.on.bind(emitter)
859
925
  }
860
- this.rules.define(key, ruleCreator);
861
926
  }
927
+ )
928
+ );
862
929
 
863
- const severity = getRuleSeverity(config.rules[key]);
864
- const options = getRuleOptions(config.rules[key]);
865
-
866
- try {
867
- const ruleContext = new RuleContext(
868
- key, this, severity, options,
869
- config.settings, config.parserOptions, config.parser,
870
- ruleCreator.meta,
871
- (parseResult && parseResult.services ? parseResult.services : {})
872
- );
930
+ // enable appropriate rules
931
+ Object.keys(config.rules).filter(ruleId => getRuleSeverity(config.rules[ruleId]) > 0).forEach(ruleId => {
932
+ let ruleCreator = this.rules.get(ruleId);
873
933
 
874
- const rule = ruleCreator.create ? ruleCreator.create(ruleContext)
875
- : ruleCreator(ruleContext);
934
+ if (!ruleCreator) {
935
+ const replacementMsg = getRuleReplacementMessage(ruleId);
876
936
 
877
- // add all the selectors from the rule as listeners
878
- Object.keys(rule).forEach(selector => {
879
- this.on(selector, timing.enabled
880
- ? timing.time(key, rule[selector])
881
- : rule[selector]
882
- );
883
- });
884
- } catch (ex) {
885
- ex.message = `Error while loading rule '${key}': ${ex.message}`;
886
- throw ex;
937
+ if (replacementMsg) {
938
+ ruleCreator = createStubRule(replacementMsg);
939
+ } else {
940
+ ruleCreator = createStubRule(`Definition for rule '${ruleId}' was not found`);
887
941
  }
888
- });
942
+ this.rules.define(ruleId, ruleCreator);
943
+ }
889
944
 
890
- // save config so rules can access as necessary
891
- this.currentConfig = config;
892
- this.traverser = new Traverser();
893
-
894
- const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
895
- const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
896
-
897
- // gather scope data that may be needed by the rules
898
- this.scopeManager = eslintScope.analyze(ast, {
899
- ignoreEval: true,
900
- nodejsScope: ecmaFeatures.globalReturn,
901
- impliedStrict: ecmaFeatures.impliedStrict,
902
- ecmaVersion,
903
- sourceType: this.currentConfig.parserOptions.sourceType || "script",
904
- fallback: Traverser.getKeys
905
- });
945
+ const severity = getRuleSeverity(config.rules[ruleId]);
946
+ const ruleContext = Object.freeze(
947
+ Object.assign(
948
+ Object.create(sharedTraversalContext),
949
+ {
950
+ id: ruleId,
951
+ options: getRuleOptions(config.rules[ruleId]),
952
+ report: lodash.flow([
953
+ createReportTranslator({ ruleId, severity, sourceCode: this.sourceCode }),
954
+ problem => {
955
+ if (problem.fix && ruleCreator.meta && !ruleCreator.meta.fixable) {
956
+ throw new Error("Fixable rules should export a `meta.fixable` property.");
957
+ }
958
+ if (!isDisabledByReportingConfig(this.reportingConfig, ruleId, problem)) {
959
+ this.messages.push(problem);
960
+ }
961
+
962
+ /*
963
+ * This is used to avoid breaking rules that used monkeypatch Linter, and relied on
964
+ * `linter.report` getting called with report info every time a rule reports a problem.
965
+ * To continue to support this, make sure that `context._linter.report` is called every
966
+ * time a problem is reported by a rule, even though `context._linter` is no longer a
967
+ * `Linter` instance.
968
+ *
969
+ * This should be removed in a major release after we create a better way to
970
+ * lint for unused disable comments.
971
+ * https://github.com/eslint/eslint/issues/9193
972
+ */
973
+ sharedTraversalContext._linter.report( // eslint-disable-line no-underscore-dangle
974
+ ruleId,
975
+ severity,
976
+ { loc: { start: { line: problem.line, column: problem.column - 1 } } },
977
+ problem.message
978
+ );
979
+ }
980
+ ])
981
+ }
982
+ )
983
+ );
906
984
 
907
- this.currentScopes = this.scopeManager.scopes;
985
+ try {
986
+ const rule = ruleCreator.create
987
+ ? ruleCreator.create(ruleContext)
988
+ : ruleCreator(ruleContext);
908
989
 
909
- // augment global scope with declared global variables
910
- addDeclaredGlobals(ast, this.currentScopes[0], this.currentConfig, this.environments);
990
+ // add all the selectors from the rule as listeners
991
+ Object.keys(rule).forEach(selector => {
992
+ emitter.on(
993
+ selector, timing.enabled
994
+ ? timing.time(ruleId, rule[selector])
995
+ : rule[selector]
996
+ );
997
+ });
998
+ } catch (ex) {
999
+ ex.message = `Error while loading rule '${ruleId}': ${ex.message}`;
1000
+ throw ex;
1001
+ }
1002
+ });
911
1003
 
912
- let eventGenerator = new NodeEventGenerator(this);
1004
+ // save config so rules can access as necessary
1005
+ this.currentConfig = config;
1006
+ this.traverser = new Traverser();
1007
+
1008
+ const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
1009
+ const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
1010
+
1011
+ // gather scope data that may be needed by the rules
1012
+ this.scopeManager = eslintScope.analyze(this.sourceCode.ast, {
1013
+ ignoreEval: true,
1014
+ nodejsScope: ecmaFeatures.globalReturn,
1015
+ impliedStrict: ecmaFeatures.impliedStrict,
1016
+ ecmaVersion,
1017
+ sourceType: this.currentConfig.parserOptions.sourceType || "script",
1018
+ fallback: Traverser.getKeys
1019
+ });
913
1020
 
914
- eventGenerator = new CodePathAnalyzer(eventGenerator);
1021
+ // augment global scope with declared global variables
1022
+ addDeclaredGlobals(this.sourceCode.ast, this.scopeManager.scopes[0], this.currentConfig, this.environments);
915
1023
 
916
- /*
917
- * Each node has a type property. Whenever a particular type of
918
- * node is found, an event is fired. This allows any listeners to
919
- * automatically be informed that this type of node has been found
920
- * and react accordingly.
921
- */
922
- this.traverser.traverse(ast, {
923
- enter(node, parent) {
924
- node.parent = parent;
925
- eventGenerator.enterNode(node);
926
- },
927
- leave(node) {
928
- eventGenerator.leaveNode(node);
929
- }
930
- });
931
- }
1024
+ const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
1025
+
1026
+ /*
1027
+ * Each node has a type property. Whenever a particular type of
1028
+ * node is found, an event is fired. This allows any listeners to
1029
+ * automatically be informed that this type of node has been found
1030
+ * and react accordingly.
1031
+ */
1032
+ this.traverser.traverse(this.sourceCode.ast, {
1033
+ enter(node, parent) {
1034
+ node.parent = parent;
1035
+ eventGenerator.enterNode(node);
1036
+ },
1037
+ leave(node) {
1038
+ eventGenerator.leaveNode(node);
1039
+ }
1040
+ });
932
1041
 
933
1042
  // sort by line and column
934
1043
  this.messages.sort((a, b) => {
@@ -944,88 +1053,6 @@ class Linter extends EventEmitter {
944
1053
  return this.messages;
945
1054
  }
946
1055
 
947
- /**
948
- * Reports a message from one of the rules.
949
- * @param {string} ruleId The ID of the rule causing the message.
950
- * @param {number} severity The severity level of the rule as configured.
951
- * @param {ASTNode} node The AST node that the message relates to.
952
- * @param {Object=} location An object containing the error line and column
953
- * numbers. If location is not provided the node's start location will
954
- * be used.
955
- * @param {string} message The actual message.
956
- * @param {Object} opts Optional template data which produces a formatted message
957
- * with symbols being replaced by this object's values.
958
- * @param {Object} fix A fix command description.
959
- * @param {Object} meta Metadata of the rule
960
- * @returns {void}
961
- */
962
- report(ruleId, severity, node, location, message, opts, fix, meta) {
963
- if (node) {
964
- assert.strictEqual(typeof node, "object", "Node must be an object");
965
- }
966
-
967
- let endLocation;
968
-
969
- if (typeof location === "string") {
970
- assert.ok(node, "Node must be provided when reporting error if location is not provided");
971
-
972
- meta = fix;
973
- fix = opts;
974
- opts = message;
975
- message = location;
976
- location = node.loc.start;
977
- endLocation = node.loc.end;
978
- } else {
979
- endLocation = location.end;
980
- }
981
-
982
- location = location.start || location;
983
-
984
- if (isDisabledByReportingConfig(this.reportingConfig, ruleId, location)) {
985
- return;
986
- }
987
-
988
- if (opts) {
989
- message = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
990
- if (term in opts) {
991
- return opts[term];
992
- }
993
-
994
- // Preserve old behavior: If parameter name not provided, don't replace it.
995
- return fullMatch;
996
- });
997
- }
998
-
999
- const problem = {
1000
- ruleId,
1001
- severity,
1002
- message,
1003
- line: location.line,
1004
- column: location.column + 1, // switch to 1-base instead of 0-base
1005
- nodeType: node && node.type,
1006
- source: this.sourceCode.lines[location.line - 1] || ""
1007
- };
1008
-
1009
- // Define endLine and endColumn if exists.
1010
- if (endLocation) {
1011
- problem.endLine = endLocation.line;
1012
- problem.endColumn = endLocation.column + 1; // switch to 1-base instead of 0-base
1013
- }
1014
-
1015
- // ensure there's range and text properties, otherwise it's not a valid fix
1016
- if (fix && Array.isArray(fix.range) && (typeof fix.text === "string")) {
1017
-
1018
- // If rule uses fix, has metadata, but has no metadata.fixable, we should throw
1019
- if (meta && !meta.fixable) {
1020
- throw new Error("Fixable rules should export a `meta.fixable` property.");
1021
- }
1022
-
1023
- problem.fix = fix;
1024
- }
1025
-
1026
- this.messages.push(problem);
1027
- }
1028
-
1029
1056
  /**
1030
1057
  * Gets the SourceCode object representing the parsed source.
1031
1058
  * @returns {SourceCode} The SourceCode object.
@@ -1083,7 +1110,7 @@ class Linter extends EventEmitter {
1083
1110
 
1084
1111
  }
1085
1112
 
1086
- return this.currentScopes[0];
1113
+ return this.scopeManager.scopes[0];
1087
1114
  }
1088
1115
 
1089
1116
  /**
@@ -1210,7 +1237,7 @@ class Linter extends EventEmitter {
1210
1237
  fixed = false,
1211
1238
  passNumber = 0;
1212
1239
  const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
1213
- const shouldFix = options && options.fix || true;
1240
+ const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
1214
1241
 
1215
1242
  /**
1216
1243
  * This loop continues until one of the following is true:
@@ -1228,7 +1255,7 @@ class Linter extends EventEmitter {
1228
1255
  messages = this.verify(text, config, options);
1229
1256
 
1230
1257
  debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
1231
- fixedResult = SourceCodeFixer.applyFixes(this.getSourceCode(), messages, shouldFix);
1258
+ fixedResult = SourceCodeFixer.applyFixes(text, messages, shouldFix);
1232
1259
 
1233
1260
  // stop if there are any syntax errors.
1234
1261
  // 'fixedResult.output' is a empty string.
@@ -1263,33 +1290,8 @@ class Linter extends EventEmitter {
1263
1290
  }
1264
1291
  }
1265
1292
 
1266
- // methods that exist on SourceCode object
1267
- const externalMethods = {
1268
- getSource: "getText",
1269
- getSourceLines: "getLines",
1270
- getAllComments: "getAllComments",
1271
- getNodeByRangeIndex: "getNodeByRangeIndex",
1272
- getComments: "getComments",
1273
- getCommentsBefore: "getCommentsBefore",
1274
- getCommentsAfter: "getCommentsAfter",
1275
- getCommentsInside: "getCommentsInside",
1276
- getJSDocComment: "getJSDocComment",
1277
- getFirstToken: "getFirstToken",
1278
- getFirstTokens: "getFirstTokens",
1279
- getLastToken: "getLastToken",
1280
- getLastTokens: "getLastTokens",
1281
- getTokenAfter: "getTokenAfter",
1282
- getTokenBefore: "getTokenBefore",
1283
- getTokenByRangeStart: "getTokenByRangeStart",
1284
- getTokens: "getTokens",
1285
- getTokensAfter: "getTokensAfter",
1286
- getTokensBefore: "getTokensBefore",
1287
- getTokensBetween: "getTokensBetween"
1288
- };
1289
-
1290
- // copy over methods
1291
- Object.keys(externalMethods).forEach(methodName => {
1292
- const exMethodName = externalMethods[methodName];
1293
+ Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).forEach(methodName => {
1294
+ const exMethodName = DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName];
1293
1295
 
1294
1296
  // Applies the SourceCode methods to the Linter prototype
1295
1297
  Object.defineProperty(Linter.prototype, methodName, {