index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. const _ = require('lodash/fp');
  2. const jsonService = require('vscode-json-languageservice');
  3. const jsonServiceHandle = jsonService.getLanguageService({});
  4. const ErrorCodes = {
  5. Undefined: 0,
  6. EnumValueMismatch: 1,
  7. UnexpectedEndOfComment: 0x101,
  8. UnexpectedEndOfString: 0x102,
  9. UnexpectedEndOfNumber: 0x103,
  10. InvalidUnicode: 0x104,
  11. InvalidEscapeCharacter: 0x105,
  12. InvalidCharacter: 0x106,
  13. PropertyExpected: 0x201,
  14. CommaExpected: 0x202,
  15. ColonExpected: 0x203,
  16. ValueExpected: 0x204,
  17. CommaOrCloseBacketExpected: 0x205,
  18. CommaOrCloseBraceExpected: 0x206,
  19. TrailingComma: 0x207,
  20. DuplicateKey: 0x208,
  21. CommentNotPermitted: 0x209,
  22. SchemaResolveError: 0x300,
  23. };
  24. const AllErrorCodes = _.values(ErrorCodes);
  25. const AllowComments = 'allowComments';
  26. const fileLintResults = {};
  27. const fileComments = {};
  28. const fileDocuments = {};
  29. const getSignature = (problem) =>
  30. `${problem.range.start.line} ${problem.range.start.character} ${problem.message}`;
  31. function getDiagnostics(jsonDocument) {
  32. return _.pipe(
  33. _.map((problem) => [getSignature(problem), problem]),
  34. _.reverse, // reverse ensure fromPairs keep first signature occurence of problem
  35. _.fromPairs
  36. )(jsonDocument.syntaxErrors);
  37. }
  38. const reportError = (filter) => (errorName, context) => {
  39. _.filter(filter, fileLintResults[context.getFilename()]).forEach((error) => {
  40. context.report({
  41. ruleId: `json/${errorName}`,
  42. message: error.message,
  43. loc: {
  44. start: {line: error.range.start.line + 1, column: error.range.start.character},
  45. end: {line: error.range.end.line + 1, column: error.range.end.character},
  46. },
  47. // later: see how to add fix
  48. });
  49. });
  50. };
  51. const reportComment = (errorName, context) => {
  52. const ruleOption = _.head(context.options);
  53. if (ruleOption === AllowComments || _.get(AllowComments, ruleOption)) return;
  54. _.forEach((comment) => {
  55. context.report({
  56. ruleId: errorName,
  57. message: 'Comment not allowed',
  58. loc: {
  59. start: {line: comment.start.line + 1, column: comment.start.character},
  60. end: {line: comment.end.line + 1, column: comment.end.character},
  61. },
  62. });
  63. }, fileComments[context.getFilename()]);
  64. };
  65. const makeRule = (errorName, reporters) => ({
  66. create(context) {
  67. return {
  68. Program() {
  69. _.flatten([reporters]).map((reporter) => reporter(errorName, context));
  70. },
  71. };
  72. },
  73. });
  74. const rules = _.pipe(
  75. _.mapKeys(_.kebabCase),
  76. _.toPairs,
  77. _.map(([errorName, errorCode]) => [
  78. errorName,
  79. makeRule(
  80. errorName,
  81. reportError((err) => err.code === errorCode)
  82. ),
  83. ]),
  84. _.fromPairs,
  85. _.assign({
  86. '*': makeRule('*', [reportError(_.constant(true)), reportComment]),
  87. json: makeRule('json', [reportError(_.constant(true)), reportComment]),
  88. unknown: makeRule('unknown', reportError(_.negate(AllErrorCodes.includes))),
  89. 'comment-not-permitted': makeRule('comment-not-permitted', reportComment),
  90. })
  91. )(ErrorCodes);
  92. const errorSignature = (err) =>
  93. ['message', 'line', 'column', 'endLine', 'endColumn'].map((field) => err[field]).join('::');
  94. const getErrorCode = _.pipe(_.get('ruleId'), _.split('/'), _.last);
  95. const processors = {
  96. '.json': {
  97. preprocess: function (text, fileName) {
  98. const textDocument = jsonService.TextDocument.create(fileName, 'json', 1, text);
  99. fileDocuments[fileName] = textDocument;
  100. const parsed = jsonServiceHandle.parseJSONDocument(textDocument);
  101. fileLintResults[fileName] = getDiagnostics(parsed);
  102. fileComments[fileName] = parsed.comments;
  103. return ['']; // sorry nothing ;)
  104. },
  105. postprocess: function (messages, fileName) {
  106. const textDocument = fileDocuments[fileName];
  107. delete fileLintResults[fileName];
  108. delete fileComments[fileName];
  109. return _.pipe(
  110. _.first,
  111. _.groupBy(errorSignature),
  112. _.mapValues((errors) => {
  113. if (errors.length === 1) return _.first(errors);
  114. // Otherwise there is two errors: the generic and specific one
  115. // json/* or json/json and json/some-code
  116. const firstErrorCode = getErrorCode(errors[0]);
  117. const isFirstGeneric = ['*', 'json'].includes(firstErrorCode);
  118. const genericError = errors[isFirstGeneric ? 0 : 1];
  119. const specificError = errors[isFirstGeneric ? 1 : 0];
  120. return genericError.severity > specificError.severity
  121. ? genericError
  122. : specificError;
  123. }),
  124. _.mapValues((error) => {
  125. const source = textDocument.getText({
  126. start: {line: error.line - 1, character: error.column},
  127. end: {line: error.endLine - 1, character: error.endColumn},
  128. });
  129. return _.assign(error, {
  130. source,
  131. column: error.column + 1,
  132. endColumn: error.endColumn + 1,
  133. });
  134. }),
  135. _.values
  136. )(messages);
  137. },
  138. },
  139. };
  140. const configs = {
  141. recommended: {
  142. plugins: ['json'],
  143. rules: {
  144. 'json/*': 'error',
  145. },
  146. },
  147. 'recommended-with-comments': {
  148. plugins: ['json'],
  149. rules: {
  150. 'json/*': ['error', {allowComments: true}],
  151. },
  152. },
  153. };
  154. module.exports = {rules, configs, processors};