markdownlint 0.23.0 → 0.25.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.
@@ -2,23 +2,27 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const fs = require("fs");
6
5
  const path = require("path");
7
6
  const { promisify } = require("util");
8
7
  const markdownIt = require("markdown-it");
8
+ const { deprecatedRuleNames } = require("./constants");
9
9
  const rules = require("./rules");
10
10
  const helpers = require("../helpers");
11
11
  const cache = require("./cache");
12
12
 
13
- const deprecatedRuleNames = [ "MD002", "MD006" ];
13
+ // @ts-ignore
14
+ // eslint-disable-next-line camelcase, max-len, no-inline-comments, no-undef
15
+ const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
16
+ // Capture native require implementation for dynamic loading of modules
14
17
 
15
18
  /**
16
19
  * Validate the list of rules for structure and reuse.
17
20
  *
18
21
  * @param {Rule[]} ruleList List of rules.
22
+ * @param {boolean} synchronous Whether to execute synchronously.
19
23
  * @returns {string} Error message if validation fails.
20
24
  */
21
- function validateRuleList(ruleList) {
25
+ function validateRuleList(ruleList, synchronous) {
22
26
  let result = null;
23
27
  if (ruleList.length === rules.length) {
24
28
  // No need to validate if only using built-in rules
@@ -58,6 +62,19 @@ function validateRuleList(ruleList) {
58
62
  ) {
59
63
  result = newError("information");
60
64
  }
65
+ if (
66
+ !result &&
67
+ (rule.asynchronous !== undefined) &&
68
+ (typeof rule.asynchronous !== "boolean")
69
+ ) {
70
+ result = newError("asynchronous");
71
+ }
72
+ if (!result && rule.asynchronous && synchronous) {
73
+ result = new Error(
74
+ "Custom rule " + rule.names.join("/") + " at index " + customIndex +
75
+ " is asynchronous and can not be used in a synchronous context."
76
+ );
77
+ }
61
78
  if (!result) {
62
79
  rule.names.forEach(function forName(name) {
63
80
  const nameUpper = name.toUpperCase();
@@ -178,27 +195,21 @@ function removeFrontMatter(content, frontMatter) {
178
195
  * @returns {void}
179
196
  */
180
197
  function annotateTokens(tokens, lines) {
181
- let tableMap = null;
198
+ let trMap = null;
182
199
  tokens.forEach(function forToken(token) {
183
- // Handle missing maps for table head/body
184
- if (
185
- (token.type === "thead_open") ||
186
- (token.type === "tbody_open")
187
- ) {
188
- tableMap = token.map.slice();
189
- } else if (
190
- (token.type === "tr_close") &&
191
- tableMap
192
- ) {
193
- tableMap[0]++;
194
- } else if (
195
- (token.type === "thead_close") ||
196
- (token.type === "tbody_close")
197
- ) {
198
- tableMap = null;
200
+ // Provide missing maps for table content
201
+ if (token.type === "tr_open") {
202
+ trMap = token.map;
203
+ } else if (token.type === "tr_close") {
204
+ trMap = null;
205
+ }
206
+ if (!token.map && trMap) {
207
+ token.map = [ ...trMap ];
199
208
  }
200
- if (tableMap && !token.map) {
201
- token.map = tableMap.slice();
209
+ // Adjust maps for math blocks
210
+ if (helpers.isMathBlock(token) && token.map[1]) {
211
+ // markdown-it-texmath plugin does not account for math_block_end
212
+ token.map[1]++;
202
213
  }
203
214
  // Update token metadata
204
215
  if (token.map) {
@@ -331,8 +342,7 @@ function getEnabledRulesPerLineNumber(
331
342
  const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
332
343
  // Helper functions
333
344
  // eslint-disable-next-line jsdoc/require-jsdoc
334
- function handleInlineConfig(perLine, forEachMatch, forEachLine) {
335
- const input = perLine ? lines : [ lines.join("\n") ];
345
+ function handleInlineConfig(input, forEachMatch, forEachLine) {
336
346
  input.forEach((line, lineIndex) => {
337
347
  if (!noInlineConfig) {
338
348
  let match = null;
@@ -363,6 +373,7 @@ function getEnabledRulesPerLineNumber(
363
373
  }
364
374
  // eslint-disable-next-line jsdoc/require-jsdoc
365
375
  function applyEnableDisable(action, parameter, state) {
376
+ state = { ...state };
366
377
  const enabled = (action.startsWith("ENABLE"));
367
378
  const items = parameter ?
368
379
  parameter.trim().toUpperCase().split(/\s+/) :
@@ -372,40 +383,42 @@ function getEnabledRulesPerLineNumber(
372
383
  state[ruleName] = enabled;
373
384
  });
374
385
  });
386
+ return state;
375
387
  }
376
388
  // eslint-disable-next-line jsdoc/require-jsdoc
377
389
  function enableDisableFile(action, parameter) {
378
390
  if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
379
- applyEnableDisable(action, parameter, enabledRules);
391
+ enabledRules = applyEnableDisable(action, parameter, enabledRules);
380
392
  }
381
393
  }
382
394
  // eslint-disable-next-line jsdoc/require-jsdoc
383
395
  function captureRestoreEnableDisable(action, parameter) {
384
396
  if (action === "CAPTURE") {
385
- capturedRules = { ...enabledRules };
397
+ capturedRules = enabledRules;
386
398
  } else if (action === "RESTORE") {
387
- enabledRules = { ...capturedRules };
399
+ enabledRules = capturedRules;
388
400
  } else if ((action === "ENABLE") || (action === "DISABLE")) {
389
- enabledRules = { ...enabledRules };
390
- applyEnableDisable(action, parameter, enabledRules);
401
+ enabledRules = applyEnableDisable(action, parameter, enabledRules);
391
402
  }
392
403
  }
393
404
  // eslint-disable-next-line jsdoc/require-jsdoc
394
405
  function updateLineState() {
395
- enabledRulesPerLineNumber.push({ ...enabledRules });
406
+ enabledRulesPerLineNumber.push(enabledRules);
396
407
  }
397
408
  // eslint-disable-next-line jsdoc/require-jsdoc
398
409
  function disableNextLine(action, parameter, lineNumber) {
399
410
  if (action === "DISABLE-NEXT-LINE") {
400
- applyEnableDisable(
401
- action,
402
- parameter,
403
- enabledRulesPerLineNumber[lineNumber + 1] || {}
404
- );
411
+ const nextLineNumber = frontMatterLines.length + lineNumber + 1;
412
+ enabledRulesPerLineNumber[nextLineNumber] =
413
+ applyEnableDisable(
414
+ action,
415
+ parameter,
416
+ enabledRulesPerLineNumber[nextLineNumber] || {}
417
+ );
405
418
  }
406
419
  }
407
420
  // Handle inline comments
408
- handleInlineConfig(false, configureFile);
421
+ handleInlineConfig([ lines.join("\n") ], configureFile);
409
422
  const effectiveConfig = getEffectiveConfig(
410
423
  ruleList, config, aliasToRuleNames);
411
424
  ruleList.forEach((rule) => {
@@ -414,9 +427,9 @@ function getEnabledRulesPerLineNumber(
414
427
  enabledRules[ruleName] = !!effectiveConfig[ruleName];
415
428
  });
416
429
  capturedRules = enabledRules;
417
- handleInlineConfig(true, enableDisableFile);
418
- handleInlineConfig(true, captureRestoreEnableDisable, updateLineState);
419
- handleInlineConfig(true, disableNextLine);
430
+ handleInlineConfig(lines, enableDisableFile);
431
+ handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
432
+ handleInlineConfig(lines, disableNextLine);
420
433
  // Return results
421
434
  return {
422
435
  effectiveConfig,
@@ -424,38 +437,6 @@ function getEnabledRulesPerLineNumber(
424
437
  };
425
438
  }
426
439
 
427
- /**
428
- * Compare function for Array.prototype.sort for ascending order of errors.
429
- *
430
- * @param {LintError} a First error.
431
- * @param {LintError} b Second error.
432
- * @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
433
- */
434
- function lineNumberComparison(a, b) {
435
- return a.lineNumber - b.lineNumber;
436
- }
437
-
438
- /**
439
- * Filter function to include everything.
440
- *
441
- * @returns {boolean} True.
442
- */
443
- function filterAllValues() {
444
- return true;
445
- }
446
-
447
- /**
448
- * Function to return unique values from a sorted errors array.
449
- *
450
- * @param {LintError} value Error instance.
451
- * @param {number} index Index in array.
452
- * @param {LintError[]} array Array of errors.
453
- * @returns {boolean} Filter value.
454
- */
455
- function uniqueFilterForSortedErrors(value, index, array) {
456
- return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
457
- }
458
-
459
440
  /**
460
441
  * Lints a string containing Markdown content.
461
442
  *
@@ -505,27 +486,28 @@ function lintContent(
505
486
  );
506
487
  // Create parameters for rules
507
488
  const params = {
508
- name,
509
- tokens,
510
- lines,
511
- frontMatterLines
489
+ "name": helpers.deepFreeze(name),
490
+ "tokens": helpers.deepFreeze(tokens),
491
+ "lines": helpers.deepFreeze(lines),
492
+ "frontMatterLines": helpers.deepFreeze(frontMatterLines)
512
493
  };
513
494
  cache.lineMetadata(helpers.getLineMetadata(params));
514
- cache.flattenedLists(helpers.flattenLists(params));
495
+ cache.flattenedLists(helpers.flattenLists(params.tokens));
496
+ cache.codeBlockAndSpanRanges(
497
+ helpers.codeBlockAndSpanRanges(params, cache.lineMetadata())
498
+ );
515
499
  // Function to run for each rule
516
- const result = (resultVersion === 0) ? {} : [];
500
+ let results = [];
517
501
  // eslint-disable-next-line jsdoc/require-jsdoc
518
502
  function forRule(rule) {
519
503
  // Configure rule
520
- const ruleNameFriendly = rule.names[0];
521
- const ruleName = ruleNameFriendly.toUpperCase();
504
+ const ruleName = rule.names[0].toUpperCase();
522
505
  params.config = effectiveConfig[ruleName];
523
506
  // eslint-disable-next-line jsdoc/require-jsdoc
524
507
  function throwError(property) {
525
508
  throw new Error(
526
509
  "Property '" + property + "' of onError parameter is incorrect.");
527
510
  }
528
- const errors = [];
529
511
  // eslint-disable-next-line jsdoc/require-jsdoc
530
512
  function onError(errorInfo) {
531
513
  if (!errorInfo ||
@@ -534,6 +516,10 @@ function lintContent(
534
516
  (errorInfo.lineNumber > lines.length)) {
535
517
  throwError("lineNumber");
536
518
  }
519
+ const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
520
+ if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
521
+ return;
522
+ }
537
523
  if (errorInfo.detail &&
538
524
  !helpers.isString(errorInfo.detail)) {
539
525
  throwError("detail");
@@ -544,12 +530,12 @@ function lintContent(
544
530
  }
545
531
  if (errorInfo.range &&
546
532
  (!Array.isArray(errorInfo.range) ||
547
- (errorInfo.range.length !== 2) ||
548
- !helpers.isNumber(errorInfo.range[0]) ||
549
- (errorInfo.range[0] < 1) ||
550
- !helpers.isNumber(errorInfo.range[1]) ||
551
- (errorInfo.range[1] < 1) ||
552
- ((errorInfo.range[0] + errorInfo.range[1] - 1) >
533
+ (errorInfo.range.length !== 2) ||
534
+ !helpers.isNumber(errorInfo.range[0]) ||
535
+ (errorInfo.range[0] < 1) ||
536
+ !helpers.isNumber(errorInfo.range[1]) ||
537
+ (errorInfo.range[1] < 1) ||
538
+ ((errorInfo.range[0] + errorInfo.range[1] - 1) >
553
539
  lines[errorInfo.lineNumber - 1].length))) {
554
540
  throwError("range");
555
541
  }
@@ -594,78 +580,114 @@ function lintContent(
594
580
  cleanFixInfo.insertText = fixInfo.insertText;
595
581
  }
596
582
  }
597
- errors.push({
598
- "lineNumber": errorInfo.lineNumber + frontMatterLines.length,
599
- "detail": errorInfo.detail || null,
600
- "context": errorInfo.context || null,
601
- "range": errorInfo.range ? [ ...errorInfo.range ] : null,
583
+ results.push({
584
+ lineNumber,
585
+ "ruleName": rule.names[0],
586
+ "ruleNames": rule.names,
587
+ "ruleDescription": rule.description,
588
+ "ruleInformation": rule.information ? rule.information.href : null,
589
+ "errorDetail": errorInfo.detail || null,
590
+ "errorContext": errorInfo.context || null,
591
+ "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
602
592
  "fixInfo": fixInfo ? cleanFixInfo : null
603
593
  });
604
594
  }
605
- // Call (possibly external) rule function
606
- if (handleRuleFailures) {
607
- try {
608
- rule.function(params, onError);
609
- } catch (error) {
610
- onError({
611
- "lineNumber": 1,
612
- "detail": `This rule threw an exception: ${error.message}`
613
- });
595
+ // Call (possibly external) rule function to report errors
596
+ const catchCallsOnError = (error) => onError({
597
+ "lineNumber": 1,
598
+ "detail": `This rule threw an exception: ${error.message || error}`
599
+ });
600
+ const invokeRuleFunction = () => rule.function(params, onError);
601
+ if (rule.asynchronous) {
602
+ // Asynchronous rule, ensure it returns a Promise
603
+ const ruleFunctionPromise =
604
+ Promise.resolve().then(invokeRuleFunction);
605
+ return handleRuleFailures ?
606
+ ruleFunctionPromise.catch(catchCallsOnError) :
607
+ ruleFunctionPromise;
608
+ }
609
+ // Synchronous rule
610
+ try {
611
+ invokeRuleFunction();
612
+ } catch (error) {
613
+ if (handleRuleFailures) {
614
+ catchCallsOnError(error);
615
+ } else {
616
+ throw error;
614
617
  }
615
- } else {
616
- rule.function(params, onError);
617
618
  }
618
- // Record any errors (significant performance benefit from length check)
619
- if (errors.length > 0) {
620
- errors.sort(lineNumberComparison);
621
- const filteredErrors = errors
622
- .filter((resultVersion === 3) ?
623
- filterAllValues :
624
- uniqueFilterForSortedErrors)
625
- .filter(function removeDisabledRules(error) {
626
- return enabledRulesPerLineNumber[error.lineNumber][ruleName];
627
- })
628
- .map(function formatResults(error) {
629
- if (resultVersion === 0) {
630
- return error.lineNumber;
631
- }
632
- const errorObject = {};
633
- errorObject.lineNumber = error.lineNumber;
634
- if (resultVersion === 1) {
635
- errorObject.ruleName = ruleNameFriendly;
636
- errorObject.ruleAlias = rule.names[1] || rule.names[0];
637
- } else {
638
- errorObject.ruleNames = rule.names;
639
- }
640
- errorObject.ruleDescription = rule.description;
641
- errorObject.ruleInformation =
642
- rule.information ? rule.information.href : null;
643
- errorObject.errorDetail = error.detail;
644
- errorObject.errorContext = error.context;
645
- errorObject.errorRange = error.range;
646
- if (resultVersion === 3) {
647
- errorObject.fixInfo = error.fixInfo;
648
- }
649
- return errorObject;
650
- });
651
- if (filteredErrors.length > 0) {
652
- if (resultVersion === 0) {
653
- result[ruleNameFriendly] = filteredErrors;
654
- } else {
655
- Array.prototype.push.apply(result, filteredErrors);
656
- }
619
+ return null;
620
+ }
621
+ // eslint-disable-next-line jsdoc/require-jsdoc
622
+ function formatResults() {
623
+ // Sort results by rule name by line number
624
+ results.sort((a, b) => (
625
+ a.ruleName.localeCompare(b.ruleName) ||
626
+ a.lineNumber - b.lineNumber
627
+ ));
628
+ if (resultVersion < 3) {
629
+ // Remove fixInfo and multiple errors for the same rule and line number
630
+ const noPrevious = {
631
+ "ruleName": null,
632
+ "lineNumber": -1
633
+ };
634
+ results = results.filter((error, index, array) => {
635
+ delete error.fixInfo;
636
+ const previous = array[index - 1] || noPrevious;
637
+ return (
638
+ (error.ruleName !== previous.ruleName) ||
639
+ (error.lineNumber !== previous.lineNumber)
640
+ );
641
+ });
642
+ }
643
+ if (resultVersion === 0) {
644
+ // Return a dictionary of rule->[line numbers]
645
+ const dictionary = {};
646
+ for (const error of results) {
647
+ const ruleLines = dictionary[error.ruleName] || [];
648
+ ruleLines.push(error.lineNumber);
649
+ dictionary[error.ruleName] = ruleLines;
650
+ }
651
+ // @ts-ignore
652
+ results = dictionary;
653
+ } else if (resultVersion === 1) {
654
+ // Use ruleAlias instead of ruleNames
655
+ for (const error of results) {
656
+ error.ruleAlias = error.ruleNames[1] || error.ruleName;
657
+ delete error.ruleNames;
658
+ }
659
+ } else {
660
+ // resultVersion 2 or 3: Remove unwanted ruleName
661
+ for (const error of results) {
662
+ delete error.ruleName;
657
663
  }
658
664
  }
665
+ return results;
659
666
  }
660
667
  // Run all rules
668
+ const ruleListAsync = ruleList.filter((rule) => rule.asynchronous);
669
+ const ruleListSync = ruleList.filter((rule) => !rule.asynchronous);
670
+ const ruleListAsyncFirst = [
671
+ ...ruleListAsync,
672
+ ...ruleListSync
673
+ ];
674
+ const callbackSuccess = () => callback(null, formatResults());
675
+ const callbackError =
676
+ (error) => callback(error instanceof Error ? error : new Error(error));
661
677
  try {
662
- ruleList.forEach(forRule);
678
+ const ruleResults = ruleListAsyncFirst.map(forRule);
679
+ if (ruleListAsync.length > 0) {
680
+ Promise.all(ruleResults.slice(0, ruleListAsync.length))
681
+ .then(callbackSuccess)
682
+ .catch(callbackError);
683
+ } else {
684
+ callbackSuccess();
685
+ }
663
686
  } catch (error) {
687
+ callbackError(error);
688
+ } finally {
664
689
  cache.clear();
665
- return callback(error);
666
690
  }
667
- cache.clear();
668
- return callback(null, result);
669
691
  }
670
692
 
671
693
  /**
@@ -679,6 +701,7 @@ function lintContent(
679
701
  * @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
680
702
  * @param {boolean} noInlineConfig Whether to allow inline configuration.
681
703
  * @param {number} resultVersion Version of the LintResults object to return.
704
+ * @param {Object} fs File system implementation.
682
705
  * @param {boolean} synchronous Whether to execute synchronously.
683
706
  * @param {Function} callback Callback (err, result) function.
684
707
  * @returns {void}
@@ -692,6 +715,7 @@ function lintFile(
692
715
  handleRuleFailures,
693
716
  noInlineConfig,
694
717
  resultVersion,
718
+ fs,
695
719
  synchronous,
696
720
  callback) {
697
721
  // eslint-disable-next-line jsdoc/require-jsdoc
@@ -704,10 +728,9 @@ function lintFile(
704
728
  }
705
729
  // Make a/synchronous call to read file
706
730
  if (synchronous) {
707
- // @ts-ignore
708
- lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
731
+ lintContentWrapper(null, fs.readFileSync(file, "utf8"));
709
732
  } else {
710
- fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
733
+ fs.readFile(file, "utf8", lintContentWrapper);
711
734
  }
712
735
  }
713
736
 
@@ -725,13 +748,13 @@ function lintInput(options, synchronous, callback) {
725
748
  callback = callback || function noop() {};
726
749
  // eslint-disable-next-line unicorn/prefer-spread
727
750
  const ruleList = rules.concat(options.customRules || []);
728
- const ruleErr = validateRuleList(ruleList);
751
+ const ruleErr = validateRuleList(ruleList, synchronous);
729
752
  if (ruleErr) {
730
753
  return callback(ruleErr);
731
754
  }
732
755
  let files = [];
733
756
  if (Array.isArray(options.files)) {
734
- files = options.files.slice();
757
+ files = [ ...options.files ];
735
758
  } else if (options.files) {
736
759
  files = [ String(options.files) ];
737
760
  }
@@ -750,98 +773,85 @@ function lintInput(options, synchronous, callback) {
750
773
  // @ts-ignore
751
774
  md.use(...plugin);
752
775
  });
776
+ const fs = options.fs || require("fs");
753
777
  const results = newResults(ruleList);
754
778
  let done = false;
755
- // Linting of strings is always synchronous
756
- let syncItem = null;
779
+ let concurrency = 0;
757
780
  // eslint-disable-next-line jsdoc/require-jsdoc
758
- function syncCallback(err, result) {
759
- if (err) {
760
- done = true;
761
- return callback(err);
781
+ function lintWorker() {
782
+ let currentItem = null;
783
+ // eslint-disable-next-line jsdoc/require-jsdoc
784
+ function lintWorkerCallback(err, result) {
785
+ concurrency--;
786
+ if (err) {
787
+ done = true;
788
+ return callback(err);
789
+ }
790
+ results[currentItem] = result;
791
+ if (!synchronous) {
792
+ lintWorker();
793
+ }
794
+ return null;
762
795
  }
763
- results[syncItem] = result;
764
- return null;
765
- }
766
- while (!done && (syncItem = stringsKeys.shift())) {
767
- lintContent(
768
- ruleList,
769
- syncItem,
770
- strings[syncItem] || "",
771
- md,
772
- config,
773
- frontMatter,
774
- handleRuleFailures,
775
- noInlineConfig,
776
- resultVersion,
777
- syncCallback
778
- );
779
- }
780
- if (synchronous) {
781
- // Lint files synchronously
782
- while (!done && (syncItem = files.shift())) {
796
+ if (done) {
797
+ // Abort for error or nothing left to do
798
+ } else if (files.length > 0) {
799
+ // Lint next file
800
+ concurrency++;
801
+ currentItem = files.shift();
783
802
  lintFile(
784
803
  ruleList,
785
- syncItem,
804
+ currentItem,
786
805
  md,
787
806
  config,
788
807
  frontMatter,
789
808
  handleRuleFailures,
790
809
  noInlineConfig,
791
810
  resultVersion,
811
+ fs,
792
812
  synchronous,
793
- syncCallback
813
+ lintWorkerCallback
794
814
  );
795
- }
796
- return done || callback(null, results);
797
- }
798
- // Lint files asynchronously
799
- let concurrency = 0;
800
- // eslint-disable-next-line jsdoc/require-jsdoc
801
- function lintConcurrently() {
802
- const asyncItem = files.shift();
803
- if (done) {
804
- // Nothing to do
805
- } else if (asyncItem) {
815
+ } else if (stringsKeys.length > 0) {
816
+ // Lint next string
806
817
  concurrency++;
807
- lintFile(
818
+ currentItem = stringsKeys.shift();
819
+ lintContent(
808
820
  ruleList,
809
- asyncItem,
821
+ currentItem,
822
+ strings[currentItem] || "",
810
823
  md,
811
824
  config,
812
825
  frontMatter,
813
826
  handleRuleFailures,
814
827
  noInlineConfig,
815
828
  resultVersion,
816
- synchronous,
817
- (err, result) => {
818
- concurrency--;
819
- if (err) {
820
- done = true;
821
- return callback(err);
822
- }
823
- results[asyncItem] = result;
824
- lintConcurrently();
825
- return null;
826
- }
829
+ lintWorkerCallback
827
830
  );
828
831
  } else if (concurrency === 0) {
832
+ // Finish
829
833
  done = true;
830
834
  return callback(null, results);
831
835
  }
832
836
  return null;
833
837
  }
834
- // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
835
- // delay suggests that a concurrency factor of 8 can eliminate the impact
836
- // of that delay (i.e., total time is the same as with no delay).
837
- lintConcurrently();
838
- lintConcurrently();
839
- lintConcurrently();
840
- lintConcurrently();
841
- lintConcurrently();
842
- lintConcurrently();
843
- lintConcurrently();
844
- lintConcurrently();
838
+ if (synchronous) {
839
+ while (!done) {
840
+ lintWorker();
841
+ }
842
+ } else {
843
+ // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
844
+ // delay suggests that a concurrency factor of 8 can eliminate the impact
845
+ // of that delay (i.e., total time is the same as with no delay).
846
+ lintWorker();
847
+ lintWorker();
848
+ lintWorker();
849
+ lintWorker();
850
+ lintWorker();
851
+ lintWorker();
852
+ lintWorker();
853
+ lintWorker();
854
+ }
845
855
  return null;
846
856
  }
847
857
 
@@ -897,12 +907,13 @@ function parseConfiguration(name, content, parsers) {
897
907
  let config = null;
898
908
  let message = "";
899
909
  const errors = [];
910
+ let index = 0;
900
911
  // Try each parser
901
912
  (parsers || [ JSON.parse ]).every((parser) => {
902
913
  try {
903
914
  config = parser(content);
904
915
  } catch (error) {
905
- errors.push(error.message);
916
+ errors.push(`Parser ${index++}: ${error.message}`);
906
917
  }
907
918
  return !config;
908
919
  });
@@ -923,22 +934,55 @@ function parseConfiguration(name, content, parsers) {
923
934
  *
924
935
  * @param {string} configFile Configuration file name.
925
936
  * @param {string} referenceId Referenced identifier to resolve.
937
+ * @param {Object} fs File system implementation.
938
+ * @param {ResolveConfigExtendsCallback} [callback] Callback (err, result)
939
+ * function.
940
+ * @returns {void}
941
+ */
942
+ function resolveConfigExtends(configFile, referenceId, fs, callback) {
943
+ const configFileDirname = path.dirname(configFile);
944
+ const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
945
+ fs.access(resolvedExtendsFile, (err) => {
946
+ if (err) {
947
+ // Not a file, try require.resolve
948
+ try {
949
+ return callback(null, dynamicRequire.resolve(
950
+ referenceId,
951
+ { "paths": [ configFileDirname ] }
952
+ ));
953
+ } catch {
954
+ // Unable to resolve, use resolvedExtendsFile
955
+ }
956
+ }
957
+ return callback(null, resolvedExtendsFile);
958
+ });
959
+ }
960
+
961
+ /**
962
+ * Resolve referenced "extends" path in a configuration file
963
+ * using path.resolve() with require.resolve() as a fallback.
964
+ *
965
+ * @param {string} configFile Configuration file name.
966
+ * @param {string} referenceId Referenced identifier to resolve.
967
+ * @param {Object} fs File system implementation.
926
968
  * @returns {string} Resolved path to file.
927
969
  */
928
- function resolveConfigExtends(configFile, referenceId) {
970
+ function resolveConfigExtendsSync(configFile, referenceId, fs) {
929
971
  const configFileDirname = path.dirname(configFile);
930
972
  const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
931
973
  try {
932
- if (fs.statSync(resolvedExtendsFile).isFile()) {
933
- return resolvedExtendsFile;
934
- }
974
+ fs.accessSync(resolvedExtendsFile);
975
+ return resolvedExtendsFile;
935
976
  } catch {
936
- // If not a file or fs.statSync throws, try require.resolve
977
+ // Not a file, try require.resolve
937
978
  }
938
979
  try {
939
- return require.resolve(referenceId, { "paths": [ configFileDirname ] });
980
+ return dynamicRequire.resolve(
981
+ referenceId,
982
+ { "paths": [ configFileDirname ] }
983
+ );
940
984
  } catch {
941
- // If require.resolve throws, return resolvedExtendsFile
985
+ // Unable to resolve, return resolvedExtendsFile
942
986
  }
943
987
  return resolvedExtendsFile;
944
988
  }
@@ -949,17 +993,26 @@ function resolveConfigExtends(configFile, referenceId) {
949
993
  * @param {string} file Configuration file name.
950
994
  * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
951
995
  * function(s).
996
+ * @param {Object} [fs] File system implementation.
952
997
  * @param {ReadConfigCallback} [callback] Callback (err, result) function.
953
998
  * @returns {void}
954
999
  */
955
- function readConfig(file, parsers, callback) {
1000
+ function readConfig(file, parsers, fs, callback) {
956
1001
  if (!callback) {
957
- // @ts-ignore
958
- callback = parsers;
959
- parsers = null;
1002
+ if (fs) {
1003
+ callback = fs;
1004
+ fs = null;
1005
+ } else {
1006
+ // @ts-ignore
1007
+ callback = parsers;
1008
+ parsers = null;
1009
+ }
1010
+ }
1011
+ if (!fs) {
1012
+ fs = require("fs");
960
1013
  }
961
1014
  // Read file
962
- fs.readFile(file, helpers.utf8Encoding, (err, content) => {
1015
+ fs.readFile(file, "utf8", (err, content) => {
963
1016
  if (err) {
964
1017
  return callback(err);
965
1018
  }
@@ -973,16 +1026,25 @@ function readConfig(file, parsers, callback) {
973
1026
  const configExtends = config.extends;
974
1027
  if (configExtends) {
975
1028
  delete config.extends;
976
- const resolvedExtends = resolveConfigExtends(file, configExtends);
977
- return readConfig(resolvedExtends, parsers, (errr, extendsConfig) => {
978
- if (errr) {
979
- return callback(errr);
980
- }
981
- return callback(null, {
982
- ...extendsConfig,
983
- ...config
984
- });
985
- });
1029
+ return resolveConfigExtends(
1030
+ file,
1031
+ configExtends,
1032
+ fs,
1033
+ (_, resolvedExtends) => readConfig(
1034
+ resolvedExtends,
1035
+ parsers,
1036
+ fs,
1037
+ (errr, extendsConfig) => {
1038
+ if (errr) {
1039
+ return callback(errr);
1040
+ }
1041
+ return callback(null, {
1042
+ ...extendsConfig,
1043
+ ...config
1044
+ });
1045
+ }
1046
+ )
1047
+ );
986
1048
  }
987
1049
  return callback(null, config);
988
1050
  });
@@ -995,11 +1057,12 @@ const readConfigPromisify = promisify && promisify(readConfig);
995
1057
  *
996
1058
  * @param {string} file Configuration file name.
997
1059
  * @param {ConfigurationParser[]} [parsers] Parsing function(s).
1060
+ * @param {Object} [fs] File system implementation.
998
1061
  * @returns {Promise<Configuration>} Configuration object.
999
1062
  */
1000
- function readConfigPromise(file, parsers) {
1063
+ function readConfigPromise(file, parsers, fs) {
1001
1064
  // @ts-ignore
1002
- return readConfigPromisify(file, parsers);
1065
+ return readConfigPromisify(file, parsers, fs);
1003
1066
  }
1004
1067
 
1005
1068
  /**
@@ -1007,12 +1070,16 @@ function readConfigPromise(file, parsers) {
1007
1070
  *
1008
1071
  * @param {string} file Configuration file name.
1009
1072
  * @param {ConfigurationParser[]} [parsers] Parsing function(s).
1073
+ * @param {Object} [fs] File system implementation.
1010
1074
  * @returns {Configuration} Configuration object.
1075
+ * @throws An Error if processing fails.
1011
1076
  */
1012
- function readConfigSync(file, parsers) {
1077
+ function readConfigSync(file, parsers, fs) {
1078
+ if (!fs) {
1079
+ fs = require("fs");
1080
+ }
1013
1081
  // Read file
1014
- // @ts-ignore
1015
- const content = fs.readFileSync(file, helpers.utf8Encoding);
1082
+ const content = fs.readFileSync(file, "utf8");
1016
1083
  // Try to parse file
1017
1084
  const { config, message } = parseConfiguration(file, content, parsers);
1018
1085
  if (!config) {
@@ -1022,9 +1089,9 @@ function readConfigSync(file, parsers) {
1022
1089
  const configExtends = config.extends;
1023
1090
  if (configExtends) {
1024
1091
  delete config.extends;
1025
- const resolvedExtends = resolveConfigExtends(file, configExtends);
1092
+ const resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs);
1026
1093
  return {
1027
- ...readConfigSync(resolvedExtends, parsers),
1094
+ ...readConfigSync(resolvedExtends, parsers, fs),
1028
1095
  ...config
1029
1096
  };
1030
1097
  }
@@ -1037,7 +1104,7 @@ function readConfigSync(file, parsers) {
1037
1104
  * @returns {string} SemVer string.
1038
1105
  */
1039
1106
  function getVersion() {
1040
- return require("../package.json").version;
1107
+ return require("./constants").version;
1041
1108
  }
1042
1109
 
1043
1110
  // Export a/synchronous/Promise APIs
@@ -1107,7 +1174,7 @@ module.exports = markdownlint;
1107
1174
  *
1108
1175
  * @typedef {Object} RuleOnErrorInfo
1109
1176
  * @property {number} lineNumber Line number (1-based).
1110
- * @property {string} [details] Details about the error.
1177
+ * @property {string} [detail] Detail about the error.
1111
1178
  * @property {string} [context] Context for the error.
1112
1179
  * @property {number[]} [range] Column number (1-based) and length.
1113
1180
  * @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
@@ -1131,6 +1198,7 @@ module.exports = markdownlint;
1131
1198
  * @property {string} description Rule description.
1132
1199
  * @property {URL} [information] Link to more information.
1133
1200
  * @property {string[]} tags Rule tag(s).
1201
+ * @property {boolean} [asynchronous] True if asynchronous.
1134
1202
  * @property {RuleFunction} function Rule implementation.
1135
1203
  */
1136
1204
 
@@ -1147,6 +1215,7 @@ module.exports = markdownlint;
1147
1215
  * @property {boolean} [noInlineConfig] True to ignore HTML directives.
1148
1216
  * @property {number} [resultVersion] Results object version.
1149
1217
  * @property {Plugin[]} [markdownItPlugins] Additional plugins.
1218
+ * @property {Object} [fs] File system implementation.
1150
1219
  */
1151
1220
 
1152
1221
  /**
@@ -1188,13 +1257,14 @@ module.exports = markdownlint;
1188
1257
  * Fix information.
1189
1258
  *
1190
1259
  * @typedef {Object} FixInfo
1260
+ * @property {number} [lineNumber] Line number (1-based).
1191
1261
  * @property {number} [editColumn] Column of the fix (1-based).
1192
1262
  * @property {number} [deleteCount] Count of characters to delete.
1193
1263
  * @property {string} [insertText] Text to insert (after deleting).
1194
1264
  */
1195
1265
 
1196
1266
  /**
1197
- * Called with the result of the lint operation.
1267
+ * Called with the result of the lint function.
1198
1268
  *
1199
1269
  * @callback LintCallback
1200
1270
  * @param {Error | null} err Error object or null.
@@ -1224,10 +1294,19 @@ module.exports = markdownlint;
1224
1294
  */
1225
1295
 
1226
1296
  /**
1227
- * Called with the result of the readConfig operation.
1297
+ * Called with the result of the readConfig function.
1228
1298
  *
1229
1299
  * @callback ReadConfigCallback
1230
1300
  * @param {Error | null} err Error object or null.
1231
1301
  * @param {Configuration} [config] Configuration object.
1232
1302
  * @returns {void}
1233
1303
  */
1304
+
1305
+ /**
1306
+ * Called with the result of the resolveConfigExtends function.
1307
+ *
1308
+ * @callback ResolveConfigExtendsCallback
1309
+ * @param {Error | null} err Error object or null.
1310
+ * @param {string} [path] Resolved path to file.
1311
+ * @returns {void}
1312
+ */