valid-title.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _utils = require("@typescript-eslint/utils");
  7. var _utils2 = require("./utils");
  8. const trimFXprefix = word => ['f', 'x'].includes(word.charAt(0)) ? word.substr(1) : word;
  9. const doesBinaryExpressionContainStringNode = binaryExp => {
  10. if ((0, _utils2.isStringNode)(binaryExp.right)) {
  11. return true;
  12. }
  13. if (binaryExp.left.type === _utils.AST_NODE_TYPES.BinaryExpression) {
  14. return doesBinaryExpressionContainStringNode(binaryExp.left);
  15. }
  16. return (0, _utils2.isStringNode)(binaryExp.left);
  17. };
  18. const quoteStringValue = node => node.type === _utils.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, _utils2.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. const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
  119. if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'describe' && (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test') {
  120. return;
  121. }
  122. const [argument] = node.arguments;
  123. if (!argument) {
  124. return;
  125. }
  126. if (!(0, _utils2.isStringNode)(argument)) {
  127. if (argument.type === _utils.AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) {
  128. return;
  129. }
  130. if (argument.type !== _utils.AST_NODE_TYPES.TemplateLiteral && !(ignoreTypeOfDescribeName && jestFnCall.type === 'describe')) {
  131. context.report({
  132. messageId: 'titleMustBeString',
  133. loc: argument.loc
  134. });
  135. }
  136. return;
  137. }
  138. const title = (0, _utils2.getStringValue)(argument);
  139. if (!title) {
  140. context.report({
  141. messageId: 'emptyTitle',
  142. data: {
  143. jestFunctionName: jestFnCall.type === 'describe' ? _utils2.DescribeAlias.describe : _utils2.TestCaseName.test
  144. },
  145. node
  146. });
  147. return;
  148. }
  149. if (disallowedWords.length > 0) {
  150. const disallowedMatch = disallowedWordsRegexp.exec(title);
  151. if (disallowedMatch) {
  152. context.report({
  153. data: {
  154. word: disallowedMatch[1]
  155. },
  156. messageId: 'disallowedWord',
  157. node: argument
  158. });
  159. return;
  160. }
  161. }
  162. if (title.trim().length !== title.length) {
  163. context.report({
  164. messageId: 'accidentalSpace',
  165. node: argument,
  166. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]) +?/u, '$1').replace(/ +?([`'"])$/u, '$1'))]
  167. });
  168. }
  169. const unprefixedName = trimFXprefix(jestFnCall.name);
  170. const [firstWord] = title.split(' ');
  171. if (firstWord.toLowerCase() === unprefixedName) {
  172. context.report({
  173. messageId: 'duplicatePrefix',
  174. node: argument,
  175. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]).+? /u, '$1'))]
  176. });
  177. }
  178. const jestFunctionName = unprefixedName;
  179. const [mustNotMatchPattern, mustNotMatchMessage] = (_mustNotMatchPatterns = mustNotMatchPatterns[jestFunctionName]) !== null && _mustNotMatchPatterns !== void 0 ? _mustNotMatchPatterns : [];
  180. if (mustNotMatchPattern) {
  181. if (mustNotMatchPattern.test(title)) {
  182. context.report({
  183. messageId: mustNotMatchMessage ? 'mustNotMatchCustom' : 'mustNotMatch',
  184. node: argument,
  185. data: {
  186. jestFunctionName,
  187. pattern: mustNotMatchPattern,
  188. message: mustNotMatchMessage
  189. }
  190. });
  191. return;
  192. }
  193. }
  194. const [mustMatchPattern, mustMatchMessage] = (_mustMatchPatterns$je = mustMatchPatterns[jestFunctionName]) !== null && _mustMatchPatterns$je !== void 0 ? _mustMatchPatterns$je : [];
  195. if (mustMatchPattern) {
  196. if (!mustMatchPattern.test(title)) {
  197. context.report({
  198. messageId: mustMatchMessage ? 'mustMatchCustom' : 'mustMatch',
  199. node: argument,
  200. data: {
  201. jestFunctionName,
  202. pattern: mustMatchPattern,
  203. message: mustMatchMessage
  204. }
  205. });
  206. return;
  207. }
  208. }
  209. }
  210. };
  211. }
  212. });
  213. exports.default = _default;