gurkencheck 0.0.10 → 0.1.0

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.
Files changed (334) hide show
  1. package/README.md +30 -11
  2. package/dist/config-parser.d.ts +15 -1
  3. package/dist/config-parser.js +58 -12
  4. package/dist/config-verifier.d.ts +0 -1
  5. package/dist/config-verifier.js +71 -3
  6. package/dist/diagnostics.d.ts +0 -1
  7. package/dist/diagnostics.js +0 -1
  8. package/dist/exit-codes.d.ts +0 -1
  9. package/dist/exit-codes.js +0 -1
  10. package/dist/feature-finder.d.ts +0 -1
  11. package/dist/feature-finder.js +29 -11
  12. package/dist/formatters/index.d.ts +0 -1
  13. package/dist/formatters/index.js +2 -3
  14. package/dist/formatters/json.d.ts +0 -1
  15. package/dist/formatters/json.js +0 -1
  16. package/dist/formatters/junit.d.ts +1 -13
  17. package/dist/formatters/junit.js +36 -11
  18. package/dist/formatters/sarif.d.ts +0 -1
  19. package/dist/formatters/sarif.js +25 -7
  20. package/dist/formatters/stylish.d.ts +0 -1
  21. package/dist/formatters/stylish.js +0 -1
  22. package/dist/formatters/tap.d.ts +0 -1
  23. package/dist/formatters/tap.js +37 -5
  24. package/dist/gherkin/dialects.d.ts +0 -1
  25. package/dist/gherkin/dialects.js +0 -1
  26. package/dist/gherkin/keywords.d.ts +6 -3
  27. package/dist/gherkin/keywords.js +10 -4
  28. package/dist/gherkin/parse.d.ts +0 -1
  29. package/dist/gherkin/parse.js +19 -7
  30. package/dist/gherkin/traverse.d.ts +0 -1
  31. package/dist/gherkin/traverse.js +0 -1
  32. package/dist/gherkin/variables.d.ts +12 -1
  33. package/dist/gherkin/variables.js +24 -6
  34. package/dist/index.d.ts +1 -2
  35. package/dist/index.js +1 -2
  36. package/dist/linter.d.ts +0 -1
  37. package/dist/linter.js +1 -2
  38. package/dist/logger.d.ts +0 -1
  39. package/dist/logger.js +0 -1
  40. package/dist/main.d.ts +0 -1
  41. package/dist/main.js +78 -27
  42. package/dist/presets.d.ts +2 -2
  43. package/dist/presets.js +0 -5
  44. package/dist/rules/allowed-tags.d.ts +0 -1
  45. package/dist/rules/allowed-tags.js +2 -1
  46. package/dist/rules/background-setup-only.d.ts +0 -1
  47. package/dist/rules/background-setup-only.js +0 -1
  48. package/dist/rules/file-name.d.ts +0 -1
  49. package/dist/rules/file-name.js +10 -1
  50. package/dist/rules/indentation.d.ts +0 -1
  51. package/dist/rules/indentation.js +24 -10
  52. package/dist/rules/index.d.ts +0 -1
  53. package/dist/rules/index.js +0 -1
  54. package/dist/rules/keywords-in-logical-order.d.ts +0 -1
  55. package/dist/rules/keywords-in-logical-order.js +0 -1
  56. package/dist/rules/max-scenarios-per-file.d.ts +0 -1
  57. package/dist/rules/max-scenarios-per-file.js +0 -1
  58. package/dist/rules/name-length.d.ts +0 -1
  59. package/dist/rules/name-length.js +5 -3
  60. package/dist/rules/new-line-at-eof.d.ts +0 -1
  61. package/dist/rules/new-line-at-eof.js +10 -2
  62. package/dist/rules/no-background-only-scenario.d.ts +0 -1
  63. package/dist/rules/no-background-only-scenario.js +0 -1
  64. package/dist/rules/no-dupe-feature-names.d.ts +0 -1
  65. package/dist/rules/no-dupe-feature-names.js +5 -1
  66. package/dist/rules/no-dupe-file-names.d.ts +0 -1
  67. package/dist/rules/no-dupe-file-names.js +0 -1
  68. package/dist/rules/no-dupe-scenario-names.d.ts +0 -1
  69. package/dist/rules/no-dupe-scenario-names.js +4 -1
  70. package/dist/rules/no-duplicate-tags.d.ts +0 -1
  71. package/dist/rules/no-duplicate-tags.js +0 -1
  72. package/dist/rules/no-empty-background.d.ts +0 -1
  73. package/dist/rules/no-empty-background.js +0 -1
  74. package/dist/rules/no-empty-file.d.ts +0 -1
  75. package/dist/rules/no-empty-file.js +0 -1
  76. package/dist/rules/no-examples-in-scenarios.d.ts +0 -1
  77. package/dist/rules/no-examples-in-scenarios.js +0 -1
  78. package/dist/rules/no-files-without-scenarios.d.ts +0 -1
  79. package/dist/rules/no-files-without-scenarios.js +0 -1
  80. package/dist/rules/no-homogenous-tags.d.ts +0 -1
  81. package/dist/rules/no-homogenous-tags.js +0 -1
  82. package/dist/rules/no-multiple-empty-lines.d.ts +0 -1
  83. package/dist/rules/no-multiple-empty-lines.js +2 -3
  84. package/dist/rules/no-partially-commented-tag-lines.d.ts +0 -1
  85. package/dist/rules/no-partially-commented-tag-lines.js +7 -5
  86. package/dist/rules/no-restricted-patterns.d.ts +0 -1
  87. package/dist/rules/no-restricted-patterns.js +64 -19
  88. package/dist/rules/no-restricted-tags.d.ts +0 -1
  89. package/dist/rules/no-restricted-tags.js +2 -1
  90. package/dist/rules/no-scenario-outlines-without-examples.d.ts +0 -1
  91. package/dist/rules/no-scenario-outlines-without-examples.js +0 -1
  92. package/dist/rules/no-scenarios-without-then.d.ts +0 -1
  93. package/dist/rules/no-scenarios-without-then.js +0 -1
  94. package/dist/rules/no-scenarios-without-when.d.ts +0 -1
  95. package/dist/rules/no-scenarios-without-when.js +0 -1
  96. package/dist/rules/no-superfluous-tags.d.ts +0 -1
  97. package/dist/rules/no-superfluous-tags.js +0 -1
  98. package/dist/rules/no-trailing-spaces.d.ts +0 -1
  99. package/dist/rules/no-trailing-spaces.js +13 -4
  100. package/dist/rules/no-undeclared-variables.d.ts +0 -1
  101. package/dist/rules/no-undeclared-variables.js +12 -11
  102. package/dist/rules/no-unnamed-features.d.ts +0 -1
  103. package/dist/rules/no-unnamed-features.js +0 -1
  104. package/dist/rules/no-unnamed-scenarios.d.ts +0 -1
  105. package/dist/rules/no-unnamed-scenarios.js +0 -1
  106. package/dist/rules/no-unused-variables.d.ts +0 -1
  107. package/dist/rules/no-unused-variables.js +0 -1
  108. package/dist/rules/one-space-between-tags.d.ts +0 -1
  109. package/dist/rules/one-space-between-tags.js +3 -2
  110. package/dist/rules/only-one-when.d.ts +0 -1
  111. package/dist/rules/only-one-when.js +5 -5
  112. package/dist/rules/required-tags.d.ts +0 -1
  113. package/dist/rules/required-tags.js +13 -3
  114. package/dist/rules/scenario-size.d.ts +0 -1
  115. package/dist/rules/scenario-size.js +0 -1
  116. package/dist/rules/use-and.d.ts +0 -1
  117. package/dist/rules/use-and.js +2 -2
  118. package/dist/rules.d.ts +0 -1
  119. package/dist/rules.js +49 -4
  120. package/dist/stats/collect.d.ts +0 -1
  121. package/dist/stats/collect.js +0 -1
  122. package/dist/stats/command.d.ts +0 -1
  123. package/dist/stats/command.js +24 -7
  124. package/dist/stats/format/index.d.ts +0 -1
  125. package/dist/stats/format/index.js +0 -1
  126. package/dist/stats/format/json.d.ts +0 -1
  127. package/dist/stats/format/json.js +0 -1
  128. package/dist/stats/format/markdown.d.ts +0 -1
  129. package/dist/stats/format/markdown.js +24 -6
  130. package/dist/stats/format/shared.d.ts +0 -1
  131. package/dist/stats/format/shared.js +0 -1
  132. package/dist/stats/format/text.d.ts +0 -1
  133. package/dist/stats/format/text.js +0 -1
  134. package/dist/stats/index.d.ts +0 -1
  135. package/dist/stats/index.js +0 -1
  136. package/dist/stats/normalise.d.ts +0 -1
  137. package/dist/stats/normalise.js +27 -14
  138. package/dist/stats/similar.d.ts +0 -1
  139. package/dist/stats/similar.js +0 -1
  140. package/dist/stats/types.d.ts +0 -1
  141. package/dist/stats/types.js +0 -1
  142. package/dist/suppressions.d.ts +6 -3
  143. package/dist/suppressions.js +36 -30
  144. package/dist/types.d.ts +7 -1
  145. package/dist/types.js +0 -1
  146. package/dist/util/case.d.ts +2 -2
  147. package/dist/util/case.js +17 -3
  148. package/dist/util/collections.d.ts +0 -1
  149. package/dist/util/collections.js +0 -1
  150. package/dist/util/glob.d.ts +9 -3
  151. package/dist/util/glob.js +115 -27
  152. package/dist/util/json.d.ts +0 -1
  153. package/dist/util/json.js +0 -1
  154. package/dist/util/lines.d.ts +6 -4
  155. package/dist/util/lines.js +22 -4
  156. package/dist/util/location.d.ts +8 -1
  157. package/dist/util/location.js +10 -1
  158. package/dist/util/patterns.d.ts +9 -0
  159. package/dist/util/patterns.js +21 -0
  160. package/dist/util/resolve.d.ts +6 -0
  161. package/dist/util/resolve.js +101 -0
  162. package/dist/util/stream.d.ts +0 -1
  163. package/dist/util/stream.js +3 -2
  164. package/dist/util/xml.d.ts +0 -1
  165. package/dist/util/xml.js +17 -3
  166. package/dist/version.d.ts +0 -1
  167. package/dist/version.js +0 -1
  168. package/dist/watch.d.ts +11 -1
  169. package/dist/watch.js +38 -3
  170. package/package.json +2 -2
  171. package/dist/config-parser.d.ts.map +0 -1
  172. package/dist/config-parser.js.map +0 -1
  173. package/dist/config-verifier.d.ts.map +0 -1
  174. package/dist/config-verifier.js.map +0 -1
  175. package/dist/diagnostics.d.ts.map +0 -1
  176. package/dist/diagnostics.js.map +0 -1
  177. package/dist/exit-codes.d.ts.map +0 -1
  178. package/dist/exit-codes.js.map +0 -1
  179. package/dist/feature-finder.d.ts.map +0 -1
  180. package/dist/feature-finder.js.map +0 -1
  181. package/dist/formatters/index.d.ts.map +0 -1
  182. package/dist/formatters/index.js.map +0 -1
  183. package/dist/formatters/json.d.ts.map +0 -1
  184. package/dist/formatters/json.js.map +0 -1
  185. package/dist/formatters/junit.d.ts.map +0 -1
  186. package/dist/formatters/junit.js.map +0 -1
  187. package/dist/formatters/sarif.d.ts.map +0 -1
  188. package/dist/formatters/sarif.js.map +0 -1
  189. package/dist/formatters/stylish.d.ts.map +0 -1
  190. package/dist/formatters/stylish.js.map +0 -1
  191. package/dist/formatters/tap.d.ts.map +0 -1
  192. package/dist/formatters/tap.js.map +0 -1
  193. package/dist/gherkin/dialects.d.ts.map +0 -1
  194. package/dist/gherkin/dialects.js.map +0 -1
  195. package/dist/gherkin/keywords.d.ts.map +0 -1
  196. package/dist/gherkin/keywords.js.map +0 -1
  197. package/dist/gherkin/parse.d.ts.map +0 -1
  198. package/dist/gherkin/parse.js.map +0 -1
  199. package/dist/gherkin/traverse.d.ts.map +0 -1
  200. package/dist/gherkin/traverse.js.map +0 -1
  201. package/dist/gherkin/variables.d.ts.map +0 -1
  202. package/dist/gherkin/variables.js.map +0 -1
  203. package/dist/index.d.ts.map +0 -1
  204. package/dist/index.js.map +0 -1
  205. package/dist/linter.d.ts.map +0 -1
  206. package/dist/linter.js.map +0 -1
  207. package/dist/logger.d.ts.map +0 -1
  208. package/dist/logger.js.map +0 -1
  209. package/dist/main.d.ts.map +0 -1
  210. package/dist/main.js.map +0 -1
  211. package/dist/presets.d.ts.map +0 -1
  212. package/dist/presets.js.map +0 -1
  213. package/dist/rules/allowed-tags.d.ts.map +0 -1
  214. package/dist/rules/allowed-tags.js.map +0 -1
  215. package/dist/rules/background-setup-only.d.ts.map +0 -1
  216. package/dist/rules/background-setup-only.js.map +0 -1
  217. package/dist/rules/file-name.d.ts.map +0 -1
  218. package/dist/rules/file-name.js.map +0 -1
  219. package/dist/rules/indentation.d.ts.map +0 -1
  220. package/dist/rules/indentation.js.map +0 -1
  221. package/dist/rules/index.d.ts.map +0 -1
  222. package/dist/rules/index.js.map +0 -1
  223. package/dist/rules/keywords-in-logical-order.d.ts.map +0 -1
  224. package/dist/rules/keywords-in-logical-order.js.map +0 -1
  225. package/dist/rules/max-scenarios-per-file.d.ts.map +0 -1
  226. package/dist/rules/max-scenarios-per-file.js.map +0 -1
  227. package/dist/rules/name-length.d.ts.map +0 -1
  228. package/dist/rules/name-length.js.map +0 -1
  229. package/dist/rules/new-line-at-eof.d.ts.map +0 -1
  230. package/dist/rules/new-line-at-eof.js.map +0 -1
  231. package/dist/rules/no-background-only-scenario.d.ts.map +0 -1
  232. package/dist/rules/no-background-only-scenario.js.map +0 -1
  233. package/dist/rules/no-dupe-feature-names.d.ts.map +0 -1
  234. package/dist/rules/no-dupe-feature-names.js.map +0 -1
  235. package/dist/rules/no-dupe-file-names.d.ts.map +0 -1
  236. package/dist/rules/no-dupe-file-names.js.map +0 -1
  237. package/dist/rules/no-dupe-scenario-names.d.ts.map +0 -1
  238. package/dist/rules/no-dupe-scenario-names.js.map +0 -1
  239. package/dist/rules/no-duplicate-tags.d.ts.map +0 -1
  240. package/dist/rules/no-duplicate-tags.js.map +0 -1
  241. package/dist/rules/no-empty-background.d.ts.map +0 -1
  242. package/dist/rules/no-empty-background.js.map +0 -1
  243. package/dist/rules/no-empty-file.d.ts.map +0 -1
  244. package/dist/rules/no-empty-file.js.map +0 -1
  245. package/dist/rules/no-examples-in-scenarios.d.ts.map +0 -1
  246. package/dist/rules/no-examples-in-scenarios.js.map +0 -1
  247. package/dist/rules/no-files-without-scenarios.d.ts.map +0 -1
  248. package/dist/rules/no-files-without-scenarios.js.map +0 -1
  249. package/dist/rules/no-homogenous-tags.d.ts.map +0 -1
  250. package/dist/rules/no-homogenous-tags.js.map +0 -1
  251. package/dist/rules/no-multiple-empty-lines.d.ts.map +0 -1
  252. package/dist/rules/no-multiple-empty-lines.js.map +0 -1
  253. package/dist/rules/no-partially-commented-tag-lines.d.ts.map +0 -1
  254. package/dist/rules/no-partially-commented-tag-lines.js.map +0 -1
  255. package/dist/rules/no-restricted-patterns.d.ts.map +0 -1
  256. package/dist/rules/no-restricted-patterns.js.map +0 -1
  257. package/dist/rules/no-restricted-tags.d.ts.map +0 -1
  258. package/dist/rules/no-restricted-tags.js.map +0 -1
  259. package/dist/rules/no-scenario-outlines-without-examples.d.ts.map +0 -1
  260. package/dist/rules/no-scenario-outlines-without-examples.js.map +0 -1
  261. package/dist/rules/no-scenarios-without-then.d.ts.map +0 -1
  262. package/dist/rules/no-scenarios-without-then.js.map +0 -1
  263. package/dist/rules/no-scenarios-without-when.d.ts.map +0 -1
  264. package/dist/rules/no-scenarios-without-when.js.map +0 -1
  265. package/dist/rules/no-superfluous-tags.d.ts.map +0 -1
  266. package/dist/rules/no-superfluous-tags.js.map +0 -1
  267. package/dist/rules/no-trailing-spaces.d.ts.map +0 -1
  268. package/dist/rules/no-trailing-spaces.js.map +0 -1
  269. package/dist/rules/no-undeclared-variables.d.ts.map +0 -1
  270. package/dist/rules/no-undeclared-variables.js.map +0 -1
  271. package/dist/rules/no-unnamed-features.d.ts.map +0 -1
  272. package/dist/rules/no-unnamed-features.js.map +0 -1
  273. package/dist/rules/no-unnamed-scenarios.d.ts.map +0 -1
  274. package/dist/rules/no-unnamed-scenarios.js.map +0 -1
  275. package/dist/rules/no-unused-variables.d.ts.map +0 -1
  276. package/dist/rules/no-unused-variables.js.map +0 -1
  277. package/dist/rules/one-space-between-tags.d.ts.map +0 -1
  278. package/dist/rules/one-space-between-tags.js.map +0 -1
  279. package/dist/rules/only-one-when.d.ts.map +0 -1
  280. package/dist/rules/only-one-when.js.map +0 -1
  281. package/dist/rules/required-tags.d.ts.map +0 -1
  282. package/dist/rules/required-tags.js.map +0 -1
  283. package/dist/rules/scenario-size.d.ts.map +0 -1
  284. package/dist/rules/scenario-size.js.map +0 -1
  285. package/dist/rules/use-and.d.ts.map +0 -1
  286. package/dist/rules/use-and.js.map +0 -1
  287. package/dist/rules.d.ts.map +0 -1
  288. package/dist/rules.js.map +0 -1
  289. package/dist/stats/collect.d.ts.map +0 -1
  290. package/dist/stats/collect.js.map +0 -1
  291. package/dist/stats/command.d.ts.map +0 -1
  292. package/dist/stats/command.js.map +0 -1
  293. package/dist/stats/format/index.d.ts.map +0 -1
  294. package/dist/stats/format/index.js.map +0 -1
  295. package/dist/stats/format/json.d.ts.map +0 -1
  296. package/dist/stats/format/json.js.map +0 -1
  297. package/dist/stats/format/markdown.d.ts.map +0 -1
  298. package/dist/stats/format/markdown.js.map +0 -1
  299. package/dist/stats/format/shared.d.ts.map +0 -1
  300. package/dist/stats/format/shared.js.map +0 -1
  301. package/dist/stats/format/text.d.ts.map +0 -1
  302. package/dist/stats/format/text.js.map +0 -1
  303. package/dist/stats/index.d.ts.map +0 -1
  304. package/dist/stats/index.js.map +0 -1
  305. package/dist/stats/normalise.d.ts.map +0 -1
  306. package/dist/stats/normalise.js.map +0 -1
  307. package/dist/stats/similar.d.ts.map +0 -1
  308. package/dist/stats/similar.js.map +0 -1
  309. package/dist/stats/types.d.ts.map +0 -1
  310. package/dist/stats/types.js.map +0 -1
  311. package/dist/suppressions.d.ts.map +0 -1
  312. package/dist/suppressions.js.map +0 -1
  313. package/dist/types.d.ts.map +0 -1
  314. package/dist/types.js.map +0 -1
  315. package/dist/util/case.d.ts.map +0 -1
  316. package/dist/util/case.js.map +0 -1
  317. package/dist/util/collections.d.ts.map +0 -1
  318. package/dist/util/collections.js.map +0 -1
  319. package/dist/util/glob.d.ts.map +0 -1
  320. package/dist/util/glob.js.map +0 -1
  321. package/dist/util/json.d.ts.map +0 -1
  322. package/dist/util/json.js.map +0 -1
  323. package/dist/util/lines.d.ts.map +0 -1
  324. package/dist/util/lines.js.map +0 -1
  325. package/dist/util/location.d.ts.map +0 -1
  326. package/dist/util/location.js.map +0 -1
  327. package/dist/util/stream.d.ts.map +0 -1
  328. package/dist/util/stream.js.map +0 -1
  329. package/dist/util/xml.d.ts.map +0 -1
  330. package/dist/util/xml.js.map +0 -1
  331. package/dist/version.d.ts.map +0 -1
  332. package/dist/version.js.map +0 -1
  333. package/dist/watch.d.ts.map +0 -1
  334. package/dist/watch.js.map +0 -1
package/README.md CHANGED
@@ -70,7 +70,8 @@ gurkencheck [options] <feature-files>
70
70
  or the path to a formatter of your own
71
71
  (default: stylish)
72
72
  -c, --config <path> configuration file (default: .gurkencheckrc)
73
- -i, --ignore <globs> comma separated globs to skip, overriding .gurkencheckignore
73
+ -i, --ignore <globs> comma separated globs to skip, overriding .gurkencheckignore;
74
+ may be given more than once
74
75
  -r, --rulesdir <dir> directory of custom rules; may be given more than once
75
76
  -l, --language <code> dialect for files with no "# language:" header
76
77
  -w, --watch keep running, checking again whenever a file changes
@@ -153,7 +154,9 @@ Statistics are for reading, not for failing a build — that is what the rules a
153
154
  gurkencheck stats [options] <feature-files>
154
155
 
155
156
  -f, --format <format> output format: text, json, md (default: text)
156
- -i, --ignore <globs> comma separated globs to skip
157
+ -c, --config <path> configuration file to take the language from
158
+ (default: .gurkencheckrc)
159
+ -i, --ignore <globs> comma separated globs to skip; may be given more than once
157
160
  -l, --language <code> dialect for files with no "# language:" header
158
161
  --top <n> how many entries each list shows (default: 10)
159
162
  -h, --help show this message
@@ -244,7 +247,8 @@ created after it started is picked up too. `node_modules` and `.git` are skipped
244
247
  ```
245
248
 
246
249
  Paths in the log are relative to the directory gurkencheck ran in, which is what code
247
- scanning needs to match a finding to a file in the repository.
250
+ scanning needs to match a finding to a file in the repository, so run it from the repository
251
+ root. A file outside that directory is named by an absolute `file:` URI instead.
248
252
 
249
253
  ## Switching a rule off for one place
250
254
 
@@ -264,7 +268,7 @@ Write a comment in the feature file:
264
268
  | Directive | What it covers |
265
269
  |---|---|
266
270
  | `gurkencheck-disable-next-line` | The line directly below the comment |
267
- | `gurkencheck-disable` | From the comment to the end of the file, or to the next `gurkencheck-enable` |
271
+ | `gurkencheck-disable` | From the comment to the end of the file, or to the next `gurkencheck-enable`. Written above the `Feature`, it also covers findings about the whole file, such as `file-name` |
268
272
  | `gurkencheck-enable` | Resumes the rules a `gurkencheck-disable` switched off |
269
273
  | `gurkencheck-disable-file` | The whole file, wherever the comment appears |
270
274
 
@@ -299,7 +303,7 @@ extends, and later entries in a list win over earlier ones.
299
303
  {
300
304
  "extends": "gurkencheck:recommended",
301
305
  "indentation": ["on", {"Step": 4}],
302
- "no-trailing-spaces": "off"
306
+ "no-dupe-scenario-names": ["on", "in-feature"]
303
307
  }
304
308
  ```
305
309
 
@@ -314,9 +318,18 @@ extends, and later entries in a list win over earlier ones.
314
318
  Put one glob per line in a `.gurkencheckignore` file, or pass `--ignore` on the command
315
319
  line. Without either, `node_modules` is skipped and everything else is checked.
316
320
 
317
- A pattern that matches a directory skips everything below it, as in `.gitignore` and
318
- `.eslintignore`, so `build` is enough and you do not have to write `build/**`. Blank lines
319
- and lines starting with `#` are ignored.
321
+ Patterns are read the way `.gitignore` reads them:
322
+
323
+ - A pattern that matches a directory skips everything below it, so `build` is enough and
324
+ you do not have to write `build/**`.
325
+ - A pattern with no slash in it, such as `build` or `*.wip.feature`, matches at any depth.
326
+ One with a slash, such as `features/legacy`, is anchored to the directory gurkencheck
327
+ runs in.
328
+ - A trailing slash, as in `tmp/`, matches only a directory.
329
+ - A leading `!` brings back something an earlier pattern skipped. The last pattern to match
330
+ decides, and nothing inside a skipped directory can be brought back.
331
+
332
+ Blank lines and lines starting with `#` are ignored.
320
333
 
321
334
  ## Custom formatters
322
335
 
@@ -370,6 +383,9 @@ npx gurkencheck --rulesdir ./rules
370
383
 
371
384
  A custom rule may reuse a built-in rule's name to replace it.
372
385
 
386
+ Every module directly inside the directory has to be a rule; TypeScript declaration files
387
+ (`.d.ts`) are skipped. Code the rules share belongs in a subdirectory, which is not read.
388
+
373
389
  `run` may also be `async` and return a promise, for a rule that has to wait for
374
390
  something — reading a file, or asking an issue tracker whether a tag refers to a real
375
391
  ticket. Files are checked one after another, so rules see a predictable order.
@@ -380,7 +396,7 @@ ticket. Files are checked one after another, so rules see a predictable order.
380
396
  import {findFeatureFiles, lint, loadRules, readConfiguration} from 'gurkencheck';
381
397
 
382
398
  const rules = await loadRules();
383
- const config = readConfiguration('.gurkencheckrc', rules);
399
+ const config = await readConfiguration('.gurkencheckrc', rules);
384
400
 
385
401
  if (config.ok) {
386
402
  const {files} = findFeatureFiles(['features']);
@@ -392,8 +408,11 @@ Each error carries `message`, `rule`, `line` and, where the rule knows one, `col
392
408
  both 1-based, so an editor can underline exactly the right text. Errors about a whole file
393
409
  or a whole line, such as a missing new line at the end of the file, have no `column`.
394
410
 
395
- Nothing in the library writes to the console or exits the process; that is the command
396
- line's job.
411
+ Nothing in the library exits the process, and the parts that lint, read configuration or
412
+ find files never write to the console; that is the command line's job. The exceptions are
413
+ the ones whose job is printing: each formatter's `printResults`, the functions in
414
+ `FORMATTERS`, and `runStats`, which runs the `stats` command and writes its report to
415
+ stdout. To get the text instead, use `toJson`, `toSarif`, or `getStatsFormatter`.
397
416
 
398
417
  ## Developing
399
418
 
@@ -7,15 +7,29 @@ export type ConfigurationResult = {
7
7
  configuration: Configuration;
8
8
  source: string;
9
9
  language?: string;
10
+ /**
11
+ * Every file the configuration was read from - the file itself and
12
+ * each one it extends, as absolute paths. What a watch has to keep an
13
+ * eye on to know the configuration changed.
14
+ */
15
+ files?: string[];
10
16
  } | {
11
17
  ok: false;
12
18
  message: string;
13
19
  details: string[];
14
20
  };
21
+ /**
22
+ * The `language` a configuration file sets, following what it extends, or
23
+ * `undefined` when there is no file, it sets none, or it cannot be read.
24
+ *
25
+ * For commands that read feature files without linting them, such as
26
+ * `stats`: they need the dialect, but should not refuse to run over a rule
27
+ * setting they never use.
28
+ */
29
+ export declare function readConfiguredLanguage(configPath?: string): Promise<string | undefined>;
15
30
  /**
16
31
  * Reads the configuration file at `configPath`, or the default file in the
17
32
  * working directory when no path is given. With neither, the recommended
18
33
  * rules are used.
19
34
  */
20
35
  export declare function readConfiguration(configPath: string | undefined, rules: RuleRegistry): Promise<ConfigurationResult>;
21
- //# sourceMappingURL=config-parser.d.ts.map
@@ -10,12 +10,12 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  * Reading and validating the configuration file.
11
11
  */
12
12
  import fs from 'node:fs';
13
- import { createRequire } from 'node:module';
14
13
  import path from 'node:path';
15
14
  import { pathToFileURL } from 'node:url';
16
15
  import { verifyConfiguration } from './config-verifier.js';
17
16
  import { PRESETS, RECOMMENDED } from './presets.js';
18
17
  import { stripJsonComments } from './util/json.js';
18
+ import { resolvePackage } from './util/resolve.js';
19
19
  /** The configuration file looked for when none is given on the command line. */
20
20
  export const DEFAULT_CONFIG_FILE_NAME = '.gurkencheckrc';
21
21
  /** The key naming the configurations to build on top of. */
@@ -30,7 +30,8 @@ function isConfigurationObject(value) {
30
30
  function parseFile(filePath) {
31
31
  let parsed;
32
32
  try {
33
- parsed = JSON.parse(stripJsonComments(fs.readFileSync(filePath, 'utf8')));
33
+ // JSON.parse refuses a byte order mark, which some editors write.
34
+ parsed = JSON.parse(stripJsonComments(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/u, '')));
34
35
  }
35
36
  catch (thrown) {
36
37
  throw new ConfigurationError(`Could not parse "${filePath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`);
@@ -80,7 +81,7 @@ async function loadExtended(specifier, fromFile) {
80
81
  }
81
82
  let resolved;
82
83
  try {
83
- resolved = createRequire(pathToFileURL(from)).resolve(specifier);
84
+ resolved = resolvePackage(specifier, from);
84
85
  }
85
86
  catch {
86
87
  throw new ConfigurationError(`Could not resolve the package "${specifier}", extended from "${fromFile}". Is it installed?`);
@@ -105,21 +106,60 @@ async function loadExtended(specifier, fromFile) {
105
106
  * Flattens a configuration and everything it extends into one object. What a
106
107
  * file says wins over what it extends, and later entries in an `extends` list
107
108
  * win over earlier ones.
109
+ *
110
+ * `ancestors` holds the chain of files that led here, not every file seen so
111
+ * far: two files extending the same base is sharing, not a cycle. `files`
112
+ * collects every file read along the way.
108
113
  */
109
- async function flatten(configuration, source, seen) {
110
- if (seen.has(source)) {
114
+ async function flatten(configuration, source, ancestors, files) {
115
+ if (ancestors.has(source)) {
111
116
  throw new ConfigurationError(`"${source}" ends up extending itself.`);
112
117
  }
113
- seen.add(source);
118
+ ancestors.add(source);
119
+ if (!Object.hasOwn(PRESETS, source)) {
120
+ files.add(path.resolve(source));
121
+ }
114
122
  let merged = {};
115
- for (const specifier of extendsList(configuration, source)) {
116
- const extended = await loadExtended(specifier, source);
117
- merged = { ...merged, ...(await flatten(extended.configuration, extended.source, seen)) };
123
+ try {
124
+ for (const specifier of extendsList(configuration, source)) {
125
+ const extended = await loadExtended(specifier, source);
126
+ merged = {
127
+ ...merged,
128
+ ...(await flatten(extended.configuration, extended.source, ancestors, files)),
129
+ };
130
+ }
131
+ }
132
+ finally {
133
+ ancestors.delete(source);
118
134
  }
119
135
  const own = { ...configuration };
120
136
  delete own[EXTENDS];
121
137
  return { ...merged, ...own };
122
138
  }
139
+ /**
140
+ * The `language` a configuration file sets, following what it extends, or
141
+ * `undefined` when there is no file, it sets none, or it cannot be read.
142
+ *
143
+ * For commands that read feature files without linting them, such as
144
+ * `stats`: they need the dialect, but should not refuse to run over a rule
145
+ * setting they never use.
146
+ */
147
+ export async function readConfiguredLanguage(configPath = DEFAULT_CONFIG_FILE_NAME) {
148
+ if (!fs.existsSync(configPath)) {
149
+ return undefined;
150
+ }
151
+ try {
152
+ const flattened = (await flatten(parseFile(configPath), configPath, new Set(), new Set()));
153
+ const language = flattened[LANGUAGE];
154
+ return typeof language === 'string' ? language : undefined;
155
+ }
156
+ catch (thrown) {
157
+ if (thrown instanceof ConfigurationError) {
158
+ return undefined;
159
+ }
160
+ throw thrown;
161
+ }
162
+ }
123
163
  /**
124
164
  * Reads the configuration file at `configPath`, or the default file in the
125
165
  * working directory when no path is given. With neither, the recommended
@@ -145,8 +185,9 @@ export async function readConfiguration(configPath, rules) {
145
185
  configPath = DEFAULT_CONFIG_FILE_NAME;
146
186
  }
147
187
  let flattened;
188
+ const files = new Set();
148
189
  try {
149
- flattened = await flatten(parseFile(configPath), configPath, new Set());
190
+ flattened = await flatten(parseFile(configPath), configPath, new Set(), files);
150
191
  }
151
192
  catch (thrown) {
152
193
  if (thrown instanceof ConfigurationError) {
@@ -172,6 +213,11 @@ export async function readConfiguration(configPath, rules) {
172
213
  if (errors.length > 0) {
173
214
  return { ok: false, message: 'Error(s) in configuration file:', details: errors };
174
215
  }
175
- return { ok: true, configuration, source: configPath, ...(language === undefined ? {} : { language }) };
216
+ return {
217
+ ok: true,
218
+ configuration,
219
+ source: configPath,
220
+ files: [...files],
221
+ ...(language === undefined ? {} : { language }),
222
+ };
176
223
  }
177
- //# sourceMappingURL=config-parser.js.map
@@ -1,4 +1,3 @@
1
1
  import type { Configuration, RuleRegistry } from './types.ts';
2
2
  /** Returns a list of problems; an empty list means the file is usable. */
3
3
  export declare function verifyConfiguration(configuration: Configuration, rules: RuleRegistry): string[];
4
- //# sourceMappingURL=config-verifier.d.ts.map
@@ -3,6 +3,7 @@
3
3
  * a rule name is reported once and clearly rather than silently ignored.
4
4
  */
5
5
  import { ALWAYS_ON_RULES } from './gherkin/parse.js';
6
+ import { BUILT_IN_RULES } from './rules/index.js';
6
7
  const STATES = ['on', 'warn', 'off'];
7
8
  function describeAllowed(availableConfigs) {
8
9
  if (Array.isArray(availableConfigs)) {
@@ -15,9 +16,69 @@ function describeAllowed(availableConfigs) {
15
16
  }
16
17
  return '';
17
18
  }
18
- function verifySettings(ruleName, availableConfigs, settings, errors) {
19
+ /** What a value should be, going by the default it would replace. */
20
+ function describeType(fallback) {
21
+ if (Array.isArray(fallback))
22
+ return 'a list of strings';
23
+ if (typeof fallback === 'number')
24
+ return 'a number';
25
+ if (typeof fallback === 'boolean')
26
+ return 'true or false';
27
+ if (typeof fallback === 'string')
28
+ return 'a string';
29
+ return 'an object';
30
+ }
31
+ /**
32
+ * Checks each setting against the type of the default it replaces, going
33
+ * into nested settings. A value of the wrong type is not a matter of taste:
34
+ * the rule would crash on it, or quietly do something else - a string where
35
+ * a list belongs is read one character at a time.
36
+ */
37
+ function verifyTypes(prefix, defaults, settings, path, errors) {
38
+ for (const [key, value] of Object.entries(settings)) {
39
+ if (!Object.hasOwn(defaults, key)) {
40
+ continue;
41
+ }
42
+ const fallback = defaults[key];
43
+ const name = `${path}${key}`;
44
+ let fits;
45
+ if (Array.isArray(fallback)) {
46
+ fits = Array.isArray(value) && value.every((entry) => typeof entry === 'string');
47
+ }
48
+ else if (typeof fallback === 'number') {
49
+ fits = typeof value === 'number' && Number.isFinite(value);
50
+ }
51
+ else if (typeof fallback === 'object' && fallback !== null) {
52
+ fits = typeof value === 'object' && value !== null && !Array.isArray(value);
53
+ if (fits) {
54
+ for (const nested of Object.keys(value)) {
55
+ if (!Object.hasOwn(fallback, nested)) {
56
+ errors.push(`${prefix}"${name}" has no setting called "${nested}". Available settings: ${describeAllowed(fallback)}`);
57
+ }
58
+ }
59
+ verifyTypes(prefix, fallback, value, `${name}.`, errors);
60
+ continue;
61
+ }
62
+ }
63
+ else {
64
+ fits = typeof value === typeof fallback;
65
+ }
66
+ if (!fits) {
67
+ errors.push(`${prefix}"${name}" should be ${describeType(fallback)}, not ${JSON.stringify(value)}`);
68
+ }
69
+ }
70
+ }
71
+ function verifySettings(rule, settings, errors) {
72
+ const ruleName = rule.name;
73
+ const availableConfigs = rule.availableConfigs;
19
74
  const prefix = `Invalid rule configuration for "${ruleName}" - `;
20
75
  if (availableConfigs === undefined) {
76
+ // A custom rule may read settings it never declared; a built-in one
77
+ // declares every setting it has, so settings for one with none are a
78
+ // mistake.
79
+ if (BUILT_IN_RULES.includes(rule)) {
80
+ errors.push(`${prefix}the rule has no settings, so the config should be "on", "warn" or "off"`);
81
+ }
21
82
  return;
22
83
  }
23
84
  if (Array.isArray(availableConfigs)) {
@@ -37,6 +98,14 @@ function verifySettings(ruleName, availableConfigs, settings, errors) {
37
98
  errors.push(`${prefix}the rule has no setting called "${key}". Available settings: ${describeAllowed(availableConfigs)}`);
38
99
  }
39
100
  }
101
+ const before = errors.length;
102
+ verifyTypes(prefix, availableConfigs, settings, '', errors);
103
+ // The rule's own checks assume the types are right, so they run only then.
104
+ if (errors.length === before && rule.verifySettings !== undefined) {
105
+ for (const problem of rule.verifySettings(settings)) {
106
+ errors.push(`${prefix}${problem}`);
107
+ }
108
+ }
40
109
  }
41
110
  /** Returns a list of problems; an empty list means the file is usable. */
42
111
  export function verifyConfiguration(configuration, rules) {
@@ -74,8 +143,7 @@ export function verifyConfiguration(configuration, rules) {
74
143
  errors.push(`${prefix}the config should have exactly 2 parts: a state and the rule's settings`);
75
144
  continue;
76
145
  }
77
- verifySettings(ruleName, rule.availableConfigs, ruleConfig[1], errors);
146
+ verifySettings(rule, ruleConfig[1], errors);
78
147
  }
79
148
  return errors;
80
149
  }
81
- //# sourceMappingURL=config-verifier.js.map
@@ -31,4 +31,3 @@ export declare function collectDiagnostics(): CollectedDiagnostics;
31
31
  * other.
32
32
  */
33
33
  export declare const TO_STDERR: Diagnostics;
34
- //# sourceMappingURL=diagnostics.d.ts.map
@@ -50,4 +50,3 @@ export const TO_STDERR = {
50
50
  }
51
51
  },
52
52
  };
53
- //# sourceMappingURL=diagnostics.js.map
@@ -7,4 +7,3 @@ export declare const EXIT_OK = 0;
7
7
  export declare const EXIT_LINT_ERRORS = 1;
8
8
  /** The command could not run: bad arguments, missing or invalid config. */
9
9
  export declare const EXIT_USAGE = 2;
10
- //# sourceMappingURL=exit-codes.d.ts.map
@@ -7,4 +7,3 @@ export const EXIT_OK = 0;
7
7
  export const EXIT_LINT_ERRORS = 1;
8
8
  /** The command could not run: bad arguments, missing or invalid config. */
9
9
  export const EXIT_USAGE = 2;
10
- //# sourceMappingURL=exit-codes.js.map
@@ -53,4 +53,3 @@ export declare function findFeatureFileStream(patterns: readonly string[], ignor
53
53
  * every change twice.
54
54
  */
55
55
  export declare function featureRoots(patterns: readonly string[]): string[];
56
- //# sourceMappingURL=feature-finder.d.ts.map
@@ -5,33 +5,51 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { uniq } from './util/collections.js';
8
- import { globRoot, globStream, globSync } from './util/glob.js';
8
+ import { escapeGlob, globRoot, globStream, globSync, literalPath } from './util/glob.js';
9
9
  /** The ignore file looked for when `--ignore` is not given. */
10
10
  export const DEFAULT_IGNORE_FILE_NAME = '.gurkencheckignore';
11
11
  /** Never worth linting, and expensive to walk into. */
12
- export const DEFAULT_IGNORED_PATTERNS = ['node_modules/**'];
12
+ export const DEFAULT_IGNORED_PATTERNS = ['node_modules'];
13
+ /** The argument with forward slashes, whatever the platform writes. */
14
+ function toSlashes(pattern) {
15
+ return pattern.split(path.sep).join('/');
16
+ }
13
17
  /**
14
18
  * Expands one command line argument into a glob that only matches feature
15
19
  * files, or `undefined` when it does not name anything usable.
20
+ *
21
+ * Something that exists is taken as a name rather than as a glob, and
22
+ * escaped, so a file or directory whose name holds glob characters - `[wip]`,
23
+ * `{x}` - is still found, and a directory called `x.feature` is searched. A
24
+ * name without glob syntax that does not exist is reported rather than
25
+ * searched for: matching nothing, it would end in a clean run.
16
26
  */
17
27
  function toFeatureGlob(pattern) {
18
28
  if (pattern === '.') {
19
29
  return '**/*.feature';
20
30
  }
31
+ let stats;
32
+ try {
33
+ stats = fs.statSync(pattern);
34
+ }
35
+ catch {
36
+ stats = undefined;
37
+ }
38
+ if (stats?.isDirectory() === true) {
39
+ return `${escapeGlob(toSlashes(pattern)).replace(/\/+$/, '')}/**/*.feature`;
40
+ }
41
+ if (stats?.isFile() === true) {
42
+ return pattern.endsWith('.feature') ? escapeGlob(toSlashes(pattern)) : undefined;
43
+ }
44
+ if (literalPath(toSlashes(pattern)) !== undefined) {
45
+ return undefined;
46
+ }
21
47
  if (/\/\*\*$/.test(pattern)) {
22
48
  return `${pattern}/*.feature`;
23
49
  }
24
50
  if (pattern.endsWith('.feature')) {
25
51
  return pattern;
26
52
  }
27
- try {
28
- if (fs.statSync(pattern).isDirectory()) {
29
- return path.join(pattern, '**/*.feature').split(path.sep).join('/');
30
- }
31
- }
32
- catch {
33
- // Reported by the caller as an invalid pattern.
34
- }
35
53
  return undefined;
36
54
  }
37
55
  /** Reads the ignore file, one glob pattern per line, ignoring blank lines. */
@@ -51,6 +69,7 @@ export function readIgnorePatterns(ignoreArgument, ignoreFileName = DEFAULT_IGNO
51
69
  return [...DEFAULT_IGNORED_PATTERNS];
52
70
  }
53
71
  return contents
72
+ .replace(/^\uFEFF/u, '')
54
73
  .split(/\r\n|\r|\n/)
55
74
  .map((line) => line.trim())
56
75
  .filter((line) => line !== '' && !line.startsWith('#'));
@@ -145,4 +164,3 @@ export function featureRoots(patterns) {
145
164
  }
146
165
  return kept;
147
166
  }
148
- //# sourceMappingURL=feature-finder.js.map
@@ -60,4 +60,3 @@ export declare function getFormatter(format: string | undefined): Formatter | un
60
60
  * `printResults`, or be the function itself.
61
61
  */
62
62
  export declare function loadFormatter(format: string | undefined, cwd?: string): Promise<Formatter>;
63
- //# sourceMappingURL=index.d.ts.map
@@ -9,7 +9,6 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  /**
10
10
  * Output formats.
11
11
  */
12
- import { createRequire } from 'node:module';
13
12
  import path from 'node:path';
14
13
  import { pathToFileURL } from 'node:url';
15
14
  import { printResults as json } from './json.js';
@@ -17,6 +16,7 @@ import { printResults as junit } from './junit.js';
17
16
  import { printResults as sarif } from './sarif.js';
18
17
  import { printResults as stylish, startRun as streamStylish } from './stylish.js';
19
18
  import { printResults as tap, startRun as streamTap } from './tap.js';
19
+ import { resolvePackage } from '../util/resolve.js';
20
20
  /** The formats accepted by `--format` without having to be loaded. */
21
21
  export const FORMATTERS = {
22
22
  stylish,
@@ -80,7 +80,7 @@ async function loadFormatterModule(specifier, cwd) {
80
80
  }
81
81
  else {
82
82
  try {
83
- resolved = createRequire(pathToFileURL(from)).resolve(specifier);
83
+ resolved = resolvePackage(specifier, from);
84
84
  }
85
85
  catch {
86
86
  throw new Error(`Unsupported format "${specifier}". Use one of ${Object.keys(FORMATTERS).join(', ')}, ` +
@@ -113,4 +113,3 @@ export async function loadFormatter(format, cwd = process.cwd()) {
113
113
  }
114
114
  return formatter;
115
115
  }
116
- //# sourceMappingURL=index.js.map
@@ -28,4 +28,3 @@ export interface JsonResult {
28
28
  export declare function toJson(results: readonly FileResult[]): JsonResult[];
29
29
  /** Writes the results as a single line of JSON. */
30
30
  export declare function printResults(results: readonly FileResult[]): void;
31
- //# sourceMappingURL=json.d.ts.map
@@ -28,4 +28,3 @@ export function toJson(results) {
28
28
  export function printResults(results) {
29
29
  console.log(JSON.stringify(toJson(results)));
30
30
  }
31
- //# sourceMappingURL=json.js.map
@@ -1,17 +1,5 @@
1
- /**
2
- * JUnit XML, the report format CI servers such as Jenkins and TeamCity read.
3
- *
4
- * Each file becomes a test suite and each finding a test case, so a build
5
- * shows one failing test per problem with its message and position, rather
6
- * than one opaque failure per file. A file with nothing to report still
7
- * contributes a passing test case, so the report says what was checked.
8
- *
9
- * A warning does not fail the run, so it is reported as a passing test case
10
- * carrying its message, rather than as a failure the build would trip over.
11
- */
12
1
  import type { FileResult } from '../types.ts';
13
2
  /** Renders the results as a JUnit report. */
14
- export declare function format(results: readonly FileResult[]): string;
3
+ export declare function format(results: readonly FileResult[], cwd?: string): string;
15
4
  /** Writes the JUnit report to stdout. */
16
5
  export declare function printResults(results: readonly FileResult[]): void;
17
- //# sourceMappingURL=junit.d.ts.map
@@ -1,3 +1,15 @@
1
+ /**
2
+ * JUnit XML, the report format CI servers such as Jenkins and TeamCity read.
3
+ *
4
+ * Each file becomes a test suite and each finding a test case, so a build
5
+ * shows one failing test per problem with its message and position, rather
6
+ * than one opaque failure per file. A file with nothing to report still
7
+ * contributes a passing test case, so the report says what was checked.
8
+ *
9
+ * A warning does not fail the run, so it is reported as a passing test case
10
+ * carrying its message, rather than as a failure the build would trip over.
11
+ */
12
+ import path from 'node:path';
1
13
  import { attributes, escapeXml, indent, XML_DECLARATION } from '../util/xml.js';
2
14
  function severityOf(error) {
3
15
  return error.severity ?? 'error';
@@ -10,32 +22,46 @@ function classNameFor(filePath) {
10
22
  .filter((part) => part !== '' && part !== '.')
11
23
  .join('.');
12
24
  }
25
+ /**
26
+ * The path to show for a file: relative to the working directory, as a CI
27
+ * server shows the repository, so suites read `features/Login.feature` and
28
+ * group as `features.Login` rather than by where the build machine keeps its
29
+ * checkout. A file outside the working directory keeps its absolute path.
30
+ */
31
+ function displayPath(filePath, cwd) {
32
+ const relative = path.relative(cwd, filePath);
33
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
34
+ return filePath;
35
+ }
36
+ return relative.split(path.sep).join('/');
37
+ }
13
38
  function positionOf(error) {
14
39
  return error.column === undefined ? `${error.line}` : `${error.line}:${error.column}`;
15
40
  }
16
- function testCase(result, error) {
41
+ function testCase(filePath, error) {
17
42
  const open = `<testcase${attributes({
18
43
  name: `${error.rule} (${positionOf(error)})`,
19
- classname: classNameFor(result.filePath),
44
+ classname: classNameFor(filePath),
20
45
  })}>`;
21
- const detail = escapeXml(`${result.filePath}:${positionOf(error)} (${error.rule}) ${error.message}`);
46
+ const detail = escapeXml(`${filePath}:${positionOf(error)} (${error.rule}) ${error.message}`);
22
47
  const body = severityOf(error) === 'warning'
23
48
  ? `<system-out>${detail}</system-out>`
24
49
  : `<failure${attributes({ message: error.message, type: error.rule })}>${detail}</failure>`;
25
50
  return `${open}\n${indent(body, 1)}\n</testcase>`;
26
51
  }
27
- function testSuite(result) {
52
+ function testSuite(result, cwd) {
53
+ const filePath = displayPath(result.filePath, cwd);
28
54
  const failures = result.errors.filter((error) => severityOf(error) === 'error').length;
29
55
  const cases = result.errors.length > 0
30
- ? result.errors.map((error) => testCase(result, error))
56
+ ? result.errors.map((error) => testCase(filePath, error))
31
57
  : [
32
58
  `<testcase${attributes({
33
- name: result.filePath,
34
- classname: classNameFor(result.filePath),
59
+ name: filePath,
60
+ classname: classNameFor(filePath),
35
61
  })}/>`,
36
62
  ];
37
63
  const open = `<testsuite${attributes({
38
- name: result.filePath,
64
+ name: filePath,
39
65
  tests: Math.max(result.errors.length, 1),
40
66
  failures,
41
67
  errors: 0,
@@ -44,12 +70,12 @@ function testSuite(result) {
44
70
  return `${open}\n${indent(cases.join('\n'), 1)}\n</testsuite>`;
45
71
  }
46
72
  /** Renders the results as a JUnit report. */
47
- export function format(results) {
73
+ export function format(results, cwd = process.cwd()) {
48
74
  const tests = results.reduce((total, result) => total + Math.max(result.errors.length, 1), 0);
49
75
  const failures = results.reduce((total, result) => total + result.errors.filter((error) => severityOf(error) === 'error').length, 0);
50
76
  const body = [
51
77
  `<testsuites${attributes({ name: 'gurkencheck', tests, failures, errors: 0 })}>`,
52
- ...results.map((result) => indent(testSuite(result), 1)),
78
+ ...results.map((result) => indent(testSuite(result, cwd), 1)),
53
79
  '</testsuites>',
54
80
  ].join('\n');
55
81
  return `${XML_DECLARATION}\n${body}`;
@@ -58,4 +84,3 @@ export function format(results) {
58
84
  export function printResults(results) {
59
85
  console.log(format(results));
60
86
  }
61
- //# sourceMappingURL=junit.js.map
@@ -3,4 +3,3 @@ import type { FileResult } from '../types.ts';
3
3
  export declare function toSarif(results: readonly FileResult[], cwd?: string): unknown;
4
4
  /** Writes the SARIF log to stdout. */
5
5
  export declare function printResults(results: readonly FileResult[]): void;
6
- //# sourceMappingURL=sarif.d.ts.map