valid-title.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _experimentalUtils = require("@typescript-eslint/experimental-utils");
  7. var _utils = require("./utils");
  8. const trimFXprefix = word => ['f', 'x'].includes(word.charAt(0)) ? word.substr(1) : word;
  9. const doesBinaryExpressionContainStringNode = binaryExp => {
  10. if ((0, _utils.isStringNode)(binaryExp.right)) {
  11. return true;
  12. }
  13. if (binaryExp.left.type === _experimentalUtils.AST_NODE_TYPES.BinaryExpression) {
  14. return doesBinaryExpressionContainStringNode(binaryExp.left);
  15. }
  16. return (0, _utils.isStringNode)(binaryExp.left);
  17. };
  18. const quoteStringValue = node => node.type === _experimentalUtils.AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
  19. const compileMatcherPattern = matcherMaybeWithMessage => {
  20. const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
  21. return [new RegExp(matcher, 'u'), message];
  22. };
  23. const compileMatcherPatterns = matchers => {
  24. if (typeof matchers === 'string' || Array.isArray(matchers)) {
  25. const compiledMatcher = compileMatcherPattern(matchers);
  26. return {
  27. describe: compiledMatcher,
  28. test: compiledMatcher,
  29. it: compiledMatcher
  30. };
  31. }
  32. return {
  33. describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
  34. test: matchers.test ? compileMatcherPattern(matchers.test) : null,
  35. it: matchers.it ? compileMatcherPattern(matchers.it) : null
  36. };
  37. };
  38. const MatcherAndMessageSchema = {
  39. type: 'array',
  40. items: {
  41. type: 'string'
  42. },
  43. minItems: 1,
  44. maxItems: 2,
  45. additionalItems: false
  46. };
  47. var _default = (0, _utils.createRule)({
  48. name: __filename,
  49. meta: {
  50. docs: {
  51. category: 'Best Practices',
  52. description: 'Enforce valid titles',
  53. recommended: 'error'
  54. },
  55. messages: {
  56. titleMustBeString: 'Title must be a string',
  57. emptyTitle: '{{ jestFunctionName }} should not have an empty title',
  58. duplicatePrefix: 'should not have duplicate prefix',
  59. accidentalSpace: 'should not have leading or trailing spaces',
  60. disallowedWord: '"{{ word }}" is not allowed in test titles.',
  61. mustNotMatch: '{{ jestFunctionName }} should not match {{ pattern }}',
  62. mustMatch: '{{ jestFunctionName }} should match {{ pattern }}',
  63. mustNotMatchCustom: '{{ message }}',
  64. mustMatchCustom: '{{ message }}'
  65. },
  66. type: 'suggestion',
  67. schema: [{
  68. type: 'object',
  69. properties: {
  70. ignoreTypeOfDescribeName: {
  71. type: 'boolean',
  72. default: false
  73. },
  74. disallowedWords: {
  75. type: 'array',
  76. items: {
  77. type: 'string'
  78. }
  79. }
  80. },
  81. patternProperties: {
  82. [/^must(?:Not)?Match$/u.source]: {
  83. oneOf: [{
  84. type: 'string'
  85. }, MatcherAndMessageSchema, {
  86. type: 'object',
  87. propertyNames: {
  88. enum: ['describe', 'test', 'it']
  89. },
  90. additionalProperties: {
  91. oneOf: [{
  92. type: 'string'
  93. }, MatcherAndMessageSchema]
  94. }
  95. }]
  96. }
  97. },
  98. additionalProperties: false
  99. }],
  100. fixable: 'code'
  101. },
  102. defaultOptions: [{
  103. ignoreTypeOfDescribeName: false,
  104. disallowedWords: []
  105. }],
  106. create(context, [{
  107. ignoreTypeOfDescribeName,
  108. disallowedWords = [],
  109. mustNotMatch,
  110. mustMatch
  111. }]) {
  112. const disallowedWordsRegexp = new RegExp(`\\b(${disallowedWords.join('|')})\\b`, 'iu');
  113. const mustNotMatchPatterns = compileMatcherPatterns(mustNotMatch !== null && mustNotMatch !== void 0 ? mustNotMatch : {});
  114. const mustMatchPatterns = compileMatcherPatterns(mustMatch !== null && mustMatch !== void 0 ? mustMatch : {});
  115. return {
  116. CallExpression(node) {
  117. var _mustNotMatchPatterns, _mustMatchPatterns$je;
  118. if (!(0, _utils.isDescribeCall)(node) && !(0, _utils.isTestCaseCall)(node)) {
  119. return;
  120. }
  121. const [argument] = node.arguments;
  122. if (!argument) {
  123. return;
  124. }
  125. if (!(0, _utils.isStringNode)(argument)) {
  126. if (argument.type === _experimentalUtils.AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) {
  127. return;
  128. }
  129. if (argument.type !== _experimentalUtils.AST_NODE_TYPES.TemplateLiteral && !(ignoreTypeOfDescribeName && (0, _utils.isDescribeCall)(node))) {
  130. context.report({
  131. messageId: 'titleMustBeString',
  132. loc: argument.loc
  133. });
  134. }
  135. return;
  136. }
  137. const title = (0, _utils.getStringValue)(argument);
  138. if (!title) {
  139. context.report({
  140. messageId: 'emptyTitle',
  141. data: {
  142. jestFunctionName: (0, _utils.isDescribeCall)(node) ? _utils.DescribeAlias.describe : _utils.TestCaseName.test
  143. },
  144. node
  145. });
  146. return;
  147. }
  148. if (disallowedWords.length > 0) {
  149. const disallowedMatch = disallowedWordsRegexp.exec(title);
  150. if (disallowedMatch) {
  151. context.report({
  152. data: {
  153. word: disallowedMatch[1]
  154. },
  155. messageId: 'disallowedWord',
  156. node: argument
  157. });
  158. return;
  159. }
  160. }
  161. if (title.trim().length !== title.length) {
  162. context.report({
  163. messageId: 'accidentalSpace',
  164. node: argument,
  165. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]) +?/u, '$1').replace(/ +?([`'"])$/u, '$1'))]
  166. });
  167. }
  168. const nodeName = trimFXprefix((0, _utils.getNodeName)(node));
  169. const [firstWord] = title.split(' ');
  170. if (firstWord.toLowerCase() === nodeName) {
  171. context.report({
  172. messageId: 'duplicatePrefix',
  173. node: argument,
  174. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]).+? /u, '$1'))]
  175. });
  176. }
  177. const [jestFunctionName] = nodeName.split('.');
  178. const [mustNotMatchPattern, mustNotMatchMessage] = (_mustNotMatchPatterns = mustNotMatchPatterns[jestFunctionName]) !== null && _mustNotMatchPatterns !== void 0 ? _mustNotMatchPatterns : [];
  179. if (mustNotMatchPattern) {
  180. if (mustNotMatchPattern.test(title)) {
  181. context.report({
  182. messageId: mustNotMatchMessage ? 'mustNotMatchCustom' : 'mustNotMatch',
  183. node: argument,
  184. data: {
  185. jestFunctionName,
  186. pattern: mustNotMatchPattern,
  187. message: mustNotMatchMessage
  188. }
  189. });
  190. return;
  191. }
  192. }
  193. const [mustMatchPattern, mustMatchMessage] = (_mustMatchPatterns$je = mustMatchPatterns[jestFunctionName]) !== null && _mustMatchPatterns$je !== void 0 ? _mustMatchPatterns$je : [];
  194. if (mustMatchPattern) {
  195. if (!mustMatchPattern.test(title)) {
  196. context.report({
  197. messageId: mustMatchMessage ? 'mustMatchCustom' : 'mustMatch',
  198. node: argument,
  199. data: {
  200. jestFunctionName,
  201. pattern: mustMatchPattern,
  202. message: mustMatchMessage
  203. }
  204. });
  205. return;
  206. }
  207. }
  208. }
  209. };
  210. }
  211. });
  212. exports.default = _default;