requireValidFileAnnotation.js.flow 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import _ from 'lodash';
  2. import {
  3. isFlowFileAnnotation,
  4. fuzzyStringMatch,
  5. } from '../utilities';
  6. const defaults = {
  7. annotationStyle: 'none',
  8. strict: false,
  9. };
  10. const looksLikeFlowFileAnnotation = (comment) => /@(?:no)?flo/ui.test(comment);
  11. const isValidAnnotationStyle = (node, style) => {
  12. if (style === 'none') {
  13. return true;
  14. }
  15. return style === node.type.toLowerCase();
  16. };
  17. const checkAnnotationSpelling = (comment) => /@[a-z]+\b/u.test(comment) && fuzzyStringMatch(comment.replace(/no/ui, ''), '@flow', 0.2);
  18. const isFlowStrict = (comment) => /^@flow\sstrict\b/u.test(comment);
  19. const noFlowAnnotation = (comment) => /^@noflow\b/u.test(comment);
  20. const schema = [
  21. {
  22. enum: ['always', 'never'],
  23. type: 'string',
  24. },
  25. {
  26. additionalProperties: false,
  27. properties: {
  28. annotationStyle: {
  29. enum: ['none', 'line', 'block'],
  30. type: 'string',
  31. },
  32. strict: {
  33. enum: [true, false],
  34. type: 'boolean',
  35. },
  36. },
  37. type: 'object',
  38. },
  39. ];
  40. const create = (context) => {
  41. const always = context.options[0] === 'always';
  42. const style = _.get(context, 'options[1].annotationStyle', defaults.annotationStyle);
  43. const flowStrict = _.get(context, 'options[1].strict', defaults.strict);
  44. return {
  45. Program(node) {
  46. const firstToken = node.tokens[0];
  47. const potentialFlowFileAnnotation = _.find(
  48. context.getSourceCode().getAllComments(),
  49. (comment) => looksLikeFlowFileAnnotation(comment.value),
  50. );
  51. if (potentialFlowFileAnnotation) {
  52. if (firstToken && firstToken.range[0] < potentialFlowFileAnnotation.range[0]) {
  53. context.report({ message: 'Flow file annotation not at the top of the file.', node: potentialFlowFileAnnotation });
  54. }
  55. const annotationValue = potentialFlowFileAnnotation.value.trim();
  56. if (isFlowFileAnnotation(annotationValue)) {
  57. if (!isValidAnnotationStyle(potentialFlowFileAnnotation, style)) {
  58. const annotation = style === 'line' ? `// ${annotationValue}` : `/* ${annotationValue} */`;
  59. context.report({
  60. fix: (fixer) => fixer.replaceTextRange(
  61. [
  62. potentialFlowFileAnnotation.range[0],
  63. potentialFlowFileAnnotation.range[1],
  64. ],
  65. annotation,
  66. ),
  67. message: `Flow file annotation style must be \`${annotation}\``,
  68. node: potentialFlowFileAnnotation,
  69. });
  70. }
  71. if (!noFlowAnnotation(annotationValue) && flowStrict && !isFlowStrict(annotationValue)) {
  72. const str = style === 'line' ? '`// @flow strict`' : '`/* @flow strict */`';
  73. context.report({
  74. fix: (fixer) => {
  75. const annotation = ['line', 'none'].includes(style) ? '// @flow strict' : '/* @flow strict */';
  76. return fixer.replaceTextRange([
  77. potentialFlowFileAnnotation.range[0],
  78. potentialFlowFileAnnotation.range[1],
  79. ], annotation);
  80. },
  81. message: `Strict Flow file annotation is required, must be ${str}`,
  82. node,
  83. });
  84. }
  85. } else if (checkAnnotationSpelling(annotationValue)) {
  86. context.report({ message: 'Misspelled or malformed Flow file annotation.', node: potentialFlowFileAnnotation });
  87. } else {
  88. context.report({ message: 'Malformed Flow file annotation.', node: potentialFlowFileAnnotation });
  89. }
  90. } else if (always && !_.get(context, 'settings[\'ft-flow\'].onlyFilesWithFlowAnnotation')) {
  91. context.report({
  92. fix: (fixer) => {
  93. let annotation;
  94. if (flowStrict) {
  95. annotation = ['line', 'none'].includes(style) ? '// @flow strict\n' : '/* @flow strict */\n';
  96. } else {
  97. annotation = ['line', 'none'].includes(style) ? '// @flow\n' : '/* @flow */\n';
  98. }
  99. const firstComment = node.comments[0];
  100. if (firstComment && firstComment.type === 'Shebang') {
  101. return fixer
  102. .replaceTextRange(
  103. [
  104. firstComment.range[1],
  105. firstComment.range[1],
  106. ],
  107. `\n${annotation.trim()}`,
  108. );
  109. }
  110. return fixer
  111. .replaceTextRange(
  112. [
  113. node.range[0],
  114. node.range[0],
  115. ],
  116. annotation,
  117. );
  118. },
  119. message: 'Flow file annotation is missing.',
  120. node,
  121. });
  122. }
  123. },
  124. };
  125. };
  126. export default {
  127. create,
  128. meta: {
  129. fixable: 'code',
  130. },
  131. schema,
  132. };