@platformos/platformos-check-common 0.0.7 → 0.0.9

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 (337) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -4
  3. package/dist/AugmentedPlatformOSDocset.d.ts +11 -0
  4. package/dist/AugmentedPlatformOSDocset.js +81 -0
  5. package/dist/AugmentedPlatformOSDocset.js.map +1 -0
  6. package/dist/JSONValidator.js +1 -1
  7. package/dist/JSONValidator.js.map +1 -1
  8. package/dist/checks/deprecated-filter/index.js +4 -41
  9. package/dist/checks/deprecated-filter/index.js.map +1 -1
  10. package/dist/checks/deprecated-tag/index.js +21 -22
  11. package/dist/checks/deprecated-tag/index.js.map +1 -1
  12. package/dist/checks/duplicate-function-arguments/index.js +1 -1
  13. package/dist/checks/duplicate-function-arguments/index.js.map +1 -1
  14. package/dist/checks/duplicate-render-partial-arguments/index.js +1 -1
  15. package/dist/checks/duplicate-render-partial-arguments/index.js.map +1 -1
  16. package/dist/checks/graphql/index.js +1 -1
  17. package/dist/checks/graphql/index.js.map +1 -1
  18. package/dist/checks/graphql-variables/index.js +4 -0
  19. package/dist/checks/graphql-variables/index.js.map +1 -1
  20. package/dist/checks/img-width-and-height/index.js +1 -1
  21. package/dist/checks/img-width-and-height/index.js.map +1 -1
  22. package/dist/checks/index.d.ts +3 -3
  23. package/dist/checks/index.js +4 -79
  24. package/dist/checks/index.js.map +1 -1
  25. package/dist/checks/json-syntax-error/index.js +1 -1
  26. package/dist/checks/json-syntax-error/index.js.map +1 -1
  27. package/dist/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.js +1 -1
  28. package/dist/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.js.map +1 -1
  29. package/dist/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.d.ts +19 -0
  30. package/dist/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.js +79 -0
  31. package/dist/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.js.map +1 -0
  32. package/dist/checks/liquid-html-syntax-error/checks/UnknownTag.d.ts +3 -0
  33. package/dist/checks/liquid-html-syntax-error/checks/UnknownTag.js +32 -0
  34. package/dist/checks/liquid-html-syntax-error/checks/UnknownTag.js.map +1 -0
  35. package/dist/checks/liquid-html-syntax-error/index.js +23 -5
  36. package/dist/checks/liquid-html-syntax-error/index.js.map +1 -1
  37. package/dist/checks/matching-translations/index.d.ts +2 -2
  38. package/dist/checks/matching-translations/index.js +114 -90
  39. package/dist/checks/matching-translations/index.js.map +1 -1
  40. package/dist/checks/metadata-params/index.js +6 -3
  41. package/dist/checks/metadata-params/index.js.map +1 -1
  42. package/dist/checks/missing-asset/index.js +1 -1
  43. package/dist/checks/missing-asset/index.js.map +1 -1
  44. package/dist/checks/missing-partial/index.d.ts +6 -0
  45. package/dist/checks/missing-partial/index.js +70 -0
  46. package/dist/checks/missing-partial/index.js.map +1 -0
  47. package/dist/checks/orphaned-partial/index.js +4 -4
  48. package/dist/checks/orphaned-partial/index.js.map +1 -1
  49. package/dist/checks/parser-blocking-script/index.js +10 -36
  50. package/dist/checks/parser-blocking-script/index.js.map +1 -1
  51. package/dist/checks/parser-blocking-script/suggestions.d.ts +1 -2
  52. package/dist/checks/parser-blocking-script/suggestions.js +1 -11
  53. package/dist/checks/parser-blocking-script/suggestions.js.map +1 -1
  54. package/dist/checks/reserved-doc-param-names/index.js +6 -5
  55. package/dist/checks/reserved-doc-param-names/index.js.map +1 -1
  56. package/dist/checks/translation-key-exists/index.js +1 -1
  57. package/dist/checks/translation-key-exists/index.js.map +1 -1
  58. package/dist/checks/unclosed-html-element/index.js +5 -1
  59. package/dist/checks/unclosed-html-element/index.js.map +1 -1
  60. package/dist/checks/undefined-object/index.js +13 -31
  61. package/dist/checks/undefined-object/index.js.map +1 -1
  62. package/dist/checks/unique-doc-param-names/index.js +1 -1
  63. package/dist/checks/unique-doc-param-names/index.js.map +1 -1
  64. package/dist/checks/unknown-filter/index.js +3 -3
  65. package/dist/checks/unknown-filter/index.js.map +1 -1
  66. package/dist/checks/unknown-property/index.js +1 -1
  67. package/dist/checks/unknown-property/index.js.map +1 -1
  68. package/dist/checks/unrecognized-render-partial-arguments/index.js +2 -2
  69. package/dist/checks/unrecognized-render-partial-arguments/index.js.map +1 -1
  70. package/dist/checks/unused-assign/index.js +1 -1
  71. package/dist/checks/unused-assign/index.js.map +1 -1
  72. package/dist/checks/unused-doc-param/index.js +1 -1
  73. package/dist/checks/unused-doc-param/index.js.map +1 -1
  74. package/dist/checks/utils.js +1 -1
  75. package/dist/checks/utils.js.map +1 -1
  76. package/dist/checks/valid-content-for-arguments/index.js +1 -1
  77. package/dist/checks/valid-content-for-arguments/index.js.map +1 -1
  78. package/dist/checks/valid-doc-param-types/index.js +4 -4
  79. package/dist/checks/valid-doc-param-types/index.js.map +1 -1
  80. package/dist/checks/valid-html-translation/index.d.ts +2 -2
  81. package/dist/checks/valid-html-translation/index.js +4 -4
  82. package/dist/checks/valid-html-translation/index.js.map +1 -1
  83. package/dist/checks/valid-json/index.js +1 -1
  84. package/dist/checks/valid-json/index.js.map +1 -1
  85. package/dist/checks/valid-render-partial-argument-types/index.js +2 -2
  86. package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
  87. package/dist/checks/variable-name/index.js +1 -1
  88. package/dist/checks/variable-name/index.js.map +1 -1
  89. package/dist/context-utils.d.ts +18 -7
  90. package/dist/context-utils.js +68 -109
  91. package/dist/context-utils.js.map +1 -1
  92. package/dist/disabled-checks/index.js +4 -2
  93. package/dist/disabled-checks/index.js.map +1 -1
  94. package/dist/doc-generator/DocBlockGenerator.d.ts +16 -0
  95. package/dist/doc-generator/DocBlockGenerator.js +464 -0
  96. package/dist/doc-generator/DocBlockGenerator.js.map +1 -0
  97. package/dist/doc-generator/index.d.ts +1 -0
  98. package/dist/doc-generator/index.js +6 -0
  99. package/dist/doc-generator/index.js.map +1 -0
  100. package/dist/find-root.d.ts +7 -10
  101. package/dist/find-root.js +10 -17
  102. package/dist/find-root.js.map +1 -1
  103. package/dist/fixes/autofix.d.ts +4 -4
  104. package/dist/fixes/autofix.js +2 -2
  105. package/dist/fixes/autofix.js.map +1 -1
  106. package/dist/fixes/correctors/index.js +4 -0
  107. package/dist/fixes/correctors/index.js.map +1 -1
  108. package/dist/index.d.ts +5 -5
  109. package/dist/index.js +50 -17
  110. package/dist/index.js.map +1 -1
  111. package/dist/jsonc/parse.d.ts +1 -1
  112. package/dist/jsonc/parse.js +1 -1
  113. package/dist/liquid-doc/arguments.d.ts +7 -8
  114. package/dist/liquid-doc/arguments.js +28 -29
  115. package/dist/liquid-doc/arguments.js.map +1 -1
  116. package/dist/liquid-doc/liquidDoc.d.ts +1 -1
  117. package/dist/liquid-doc/liquidDoc.js.map +1 -1
  118. package/dist/liquid-doc/utils.d.ts +3 -3
  119. package/dist/liquid-doc/utils.js +14 -3
  120. package/dist/liquid-doc/utils.js.map +1 -1
  121. package/dist/path.d.ts +1 -0
  122. package/dist/path.js +16 -1
  123. package/dist/path.js.map +1 -1
  124. package/{src/test/MockTheme.ts → dist/test/MockApp.d.ts} +2 -3
  125. package/dist/test/MockApp.js +16 -0
  126. package/dist/test/MockApp.js.map +1 -0
  127. package/dist/test/MockFileSystem.d.ts +3 -3
  128. package/dist/test/MockFileSystem.js +6 -6
  129. package/dist/test/MockFileSystem.js.map +1 -1
  130. package/dist/test/index.d.ts +1 -1
  131. package/dist/test/index.js +1 -1
  132. package/dist/test/index.js.map +1 -1
  133. package/dist/test/test-helper.d.ts +10 -9
  134. package/dist/test/test-helper.js +15 -106
  135. package/dist/test/test-helper.js.map +1 -1
  136. package/dist/to-schema.d.ts +1 -1
  137. package/dist/to-source-code.d.ts +3 -2
  138. package/dist/to-source-code.js +20 -0
  139. package/dist/to-source-code.js.map +1 -1
  140. package/dist/tsconfig.tsbuildinfo +1 -1
  141. package/dist/types/platformos-liquid-docs.d.ts +128 -0
  142. package/dist/types/platformos-liquid-docs.js +3 -0
  143. package/dist/types/platformos-liquid-docs.js.map +1 -0
  144. package/dist/types/schemas/index.d.ts +0 -2
  145. package/dist/types/schemas/index.js.map +1 -1
  146. package/dist/types.d.ts +26 -67
  147. package/dist/types.js +3 -5
  148. package/dist/types.js.map +1 -1
  149. package/dist/utils/block.js.map +1 -1
  150. package/dist/utils/index.d.ts +0 -1
  151. package/dist/utils/index.js +0 -1
  152. package/dist/utils/index.js.map +1 -1
  153. package/dist/yaml/parse.d.ts +5 -0
  154. package/dist/yaml/parse.js +94 -0
  155. package/dist/yaml/parse.js.map +1 -0
  156. package/package.json +4 -3
  157. package/src/{AugmentedThemeDocset.spec.ts → AugmentedPlatformOSDocset.spec.ts} +47 -34
  158. package/src/AugmentedPlatformOSDocset.ts +89 -0
  159. package/src/JSONValidator.ts +1 -1
  160. package/src/checks/deprecated-filter/index.spec.ts +76 -248
  161. package/src/checks/deprecated-filter/index.ts +5 -53
  162. package/src/checks/deprecated-tag/index.spec.ts +85 -34
  163. package/src/checks/deprecated-tag/index.ts +27 -22
  164. package/src/checks/duplicate-function-arguments/index.ts +1 -1
  165. package/src/checks/duplicate-render-partial-arguments/index.spec.ts +12 -12
  166. package/src/checks/duplicate-render-partial-arguments/index.ts +1 -1
  167. package/src/checks/graphql/index.ts +1 -1
  168. package/src/checks/graphql-variables/index.spec.ts +95 -0
  169. package/src/checks/graphql-variables/index.ts +4 -0
  170. package/src/checks/img-width-and-height/index.ts +2 -2
  171. package/src/checks/index.ts +11 -80
  172. package/src/checks/invalid-hash-assign-target/index.spec.ts +27 -27
  173. package/src/checks/json-syntax-error/index.ts +2 -2
  174. package/src/checks/liquid-html-syntax-error/checks/InvalidBooleanExpression.spec.ts +0 -11
  175. package/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.spec.ts +1 -2
  176. package/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.ts +2 -2
  177. package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.spec.ts +259 -0
  178. package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.ts +89 -0
  179. package/src/checks/liquid-html-syntax-error/checks/UnknownTag.spec.ts +293 -0
  180. package/src/checks/liquid-html-syntax-error/checks/UnknownTag.ts +43 -0
  181. package/src/checks/liquid-html-syntax-error/index.spec.ts +1 -6
  182. package/src/checks/liquid-html-syntax-error/index.ts +26 -5
  183. package/src/checks/matching-translations/index.spec.ts +187 -354
  184. package/src/checks/matching-translations/index.ts +117 -107
  185. package/src/checks/metadata-params/index.ts +6 -8
  186. package/src/checks/missing-asset/index.ts +1 -1
  187. package/src/checks/{missing-template → missing-partial}/index.spec.ts +6 -6
  188. package/src/checks/{missing-template → missing-partial}/index.ts +12 -26
  189. package/src/checks/orphaned-partial/index.ts +3 -3
  190. package/src/checks/parser-blocking-script/index.spec.ts +0 -118
  191. package/src/checks/parser-blocking-script/index.ts +3 -33
  192. package/src/checks/parser-blocking-script/suggestions.ts +1 -28
  193. package/src/checks/translation-key-exists/index.ts +1 -1
  194. package/src/checks/unclosed-html-element/index.ts +5 -1
  195. package/src/checks/undefined-object/index.spec.ts +32 -111
  196. package/src/checks/undefined-object/index.ts +15 -34
  197. package/src/checks/unique-doc-param-names/index.ts +1 -1
  198. package/src/checks/unknown-filter/index.spec.ts +2 -2
  199. package/src/checks/unknown-filter/index.ts +3 -3
  200. package/src/checks/unknown-property/index.ts +1 -1
  201. package/src/checks/unrecognized-render-partial-arguments/index.spec.ts +5 -5
  202. package/src/checks/unrecognized-render-partial-arguments/index.ts +2 -5
  203. package/src/checks/unused-assign/index.spec.ts +0 -30
  204. package/src/checks/unused-assign/index.ts +2 -2
  205. package/src/checks/unused-doc-param/index.ts +1 -1
  206. package/src/checks/utils.ts +1 -1
  207. package/src/checks/valid-doc-param-types/index.ts +4 -4
  208. package/src/checks/valid-html-translation/index.spec.ts +42 -32
  209. package/src/checks/valid-html-translation/index.ts +7 -7
  210. package/src/checks/valid-json/index.ts +2 -2
  211. package/src/checks/valid-render-partial-argument-types/index.spec.ts +13 -13
  212. package/src/checks/valid-render-partial-argument-types/index.ts +2 -5
  213. package/src/checks/variable-name/index.ts +1 -1
  214. package/src/context-utils.spec.ts +49 -77
  215. package/src/context-utils.ts +81 -129
  216. package/src/disabled-checks/index.spec.ts +26 -26
  217. package/src/disabled-checks/index.ts +2 -2
  218. package/src/disabled-checks/test-checks.ts +4 -4
  219. package/src/find-root.ts +12 -22
  220. package/src/fixes/autofix.spec.ts +2 -2
  221. package/src/fixes/autofix.ts +4 -4
  222. package/src/fixes/correctors/index.ts +4 -0
  223. package/src/ignore.spec.ts +4 -5
  224. package/src/index.ts +51 -21
  225. package/src/jsonc/parse.ts +1 -1
  226. package/src/liquid-doc/arguments.spec.ts +19 -45
  227. package/src/liquid-doc/arguments.ts +35 -42
  228. package/src/liquid-doc/liquidDoc.spec.ts +1 -1
  229. package/src/liquid-doc/liquidDoc.ts +1 -2
  230. package/src/liquid-doc/utils.ts +17 -8
  231. package/src/path.ts +16 -0
  232. package/src/test/MockApp.ts +17 -0
  233. package/src/test/MockFileSystem.spec.ts +10 -11
  234. package/src/test/MockFileSystem.ts +6 -6
  235. package/src/test/contain-offense.spec.ts +11 -3
  236. package/src/test/index.ts +1 -1
  237. package/src/test/test-helper.ts +43 -145
  238. package/src/to-source-code.ts +20 -1
  239. package/src/types/{theme-liquid-docs.ts → platformos-liquid-docs.ts} +8 -13
  240. package/src/types.ts +29 -92
  241. package/src/utils/index.ts +0 -1
  242. package/src/visitor.spec.ts +2 -2
  243. package/src/yaml/parse.ts +111 -0
  244. package/src/AugmentedThemeDocset.ts +0 -137
  245. package/src/checks/app-block-missing-schema/index.spec.ts +0 -121
  246. package/src/checks/app-block-missing-schema/index.ts +0 -46
  247. package/src/checks/app-block-valid-tags/index.spec.ts +0 -96
  248. package/src/checks/app-block-valid-tags/index.ts +0 -54
  249. package/src/checks/asset-preload/index.spec.ts +0 -78
  250. package/src/checks/asset-preload/index.ts +0 -65
  251. package/src/checks/asset-size-app-block-css/index.spec.ts +0 -88
  252. package/src/checks/asset-size-app-block-css/index.ts +0 -78
  253. package/src/checks/asset-size-app-block-javascript/index.spec.ts +0 -66
  254. package/src/checks/asset-size-app-block-javascript/index.ts +0 -78
  255. package/src/checks/asset-size-css/index.spec.ts +0 -166
  256. package/src/checks/asset-size-css/index.ts +0 -160
  257. package/src/checks/asset-size-javascript/index.spec.ts +0 -184
  258. package/src/checks/asset-size-javascript/index.ts +0 -144
  259. package/src/checks/block-id-usage/index.spec.ts +0 -76
  260. package/src/checks/block-id-usage/index.ts +0 -72
  261. package/src/checks/cdn-preconnect/index.spec.ts +0 -40
  262. package/src/checks/cdn-preconnect/index.ts +0 -43
  263. package/src/checks/content-for-header-modification/index.spec.ts +0 -65
  264. package/src/checks/content-for-header-modification/index.ts +0 -72
  265. package/src/checks/deprecate-bgsizes/index.spec.ts +0 -41
  266. package/src/checks/deprecate-bgsizes/index.ts +0 -49
  267. package/src/checks/deprecate-lazysizes/index.spec.ts +0 -26
  268. package/src/checks/deprecate-lazysizes/index.ts +0 -58
  269. package/src/checks/deprecated-filter/fixes.ts +0 -264
  270. package/src/checks/deprecated-fonts-on-sections-and-blocks/deprecated-fonts-data.ts +0 -1343
  271. package/src/checks/deprecated-fonts-on-sections-and-blocks/index.spec.ts +0 -613
  272. package/src/checks/deprecated-fonts-on-sections-and-blocks/index.ts +0 -284
  273. package/src/checks/deprecated-fonts-on-settings-schema/index.spec.ts +0 -102
  274. package/src/checks/deprecated-fonts-on-settings-schema/index.ts +0 -66
  275. package/src/checks/duplicate-content-for-arguments/index.spec.ts +0 -98
  276. package/src/checks/duplicate-content-for-arguments/index.ts +0 -43
  277. package/src/checks/empty-block-content/index.spec.ts +0 -117
  278. package/src/checks/empty-block-content/index.ts +0 -60
  279. package/src/checks/hardcoded-routes/index.spec.ts +0 -58
  280. package/src/checks/hardcoded-routes/index.ts +0 -100
  281. package/src/checks/json-missing-block/index.spec.ts +0 -435
  282. package/src/checks/json-missing-block/index.ts +0 -56
  283. package/src/checks/json-missing-block/missing-block-utils.ts +0 -147
  284. package/src/checks/liquid-free-settings/index.spec.ts +0 -180
  285. package/src/checks/liquid-free-settings/index.ts +0 -79
  286. package/src/checks/missing-content-for-arguments/index.spec.ts +0 -144
  287. package/src/checks/missing-content-for-arguments/index.ts +0 -46
  288. package/src/checks/pagination-size/index.spec.ts +0 -158
  289. package/src/checks/pagination-size/index.ts +0 -104
  290. package/src/checks/remote-asset/index.spec.ts +0 -280
  291. package/src/checks/remote-asset/index.ts +0 -238
  292. package/src/checks/reserved-doc-param-names/index.spec.ts +0 -62
  293. package/src/checks/reserved-doc-param-names/index.ts +0 -57
  294. package/src/checks/schema-presets-block-order/index.spec.ts +0 -344
  295. package/src/checks/schema-presets-block-order/index.ts +0 -154
  296. package/src/checks/schema-presets-static-blocks/index.spec.ts +0 -145
  297. package/src/checks/schema-presets-static-blocks/index.ts +0 -126
  298. package/src/checks/static-stylesheet-and-javascript-tags/index.spec.ts +0 -257
  299. package/src/checks/static-stylesheet-and-javascript-tags/index.ts +0 -48
  300. package/src/checks/unique-settings-id/index.spec.ts +0 -24
  301. package/src/checks/unique-settings-id/index.ts +0 -84
  302. package/src/checks/unique-settings-id/test-data.ts +0 -1191
  303. package/src/checks/unique-static-block-id/index.spec.ts +0 -55
  304. package/src/checks/unique-static-block-id/index.ts +0 -60
  305. package/src/checks/unrecognized-content-for-arguments/index.spec.ts +0 -145
  306. package/src/checks/unrecognized-content-for-arguments/index.ts +0 -55
  307. package/src/checks/valid-block-target/index.spec.ts +0 -1396
  308. package/src/checks/valid-block-target/index.ts +0 -142
  309. package/src/checks/valid-content-for-argument-types/index.spec.ts +0 -382
  310. package/src/checks/valid-content-for-argument-types/index.ts +0 -42
  311. package/src/checks/valid-content-for-arguments/index.spec.ts +0 -107
  312. package/src/checks/valid-content-for-arguments/index.ts +0 -98
  313. package/src/checks/valid-local-blocks/index.spec.ts +0 -286
  314. package/src/checks/valid-local-blocks/index.ts +0 -100
  315. package/src/checks/valid-local-blocks/valid-block-utils.ts +0 -97
  316. package/src/checks/valid-schema/index.spec.ts +0 -174
  317. package/src/checks/valid-schema/index.ts +0 -41
  318. package/src/checks/valid-schema-name/index.spec.ts +0 -112
  319. package/src/checks/valid-schema-name/index.ts +0 -75
  320. package/src/checks/valid-settings-key/index.spec.ts +0 -321
  321. package/src/checks/valid-settings-key/index.ts +0 -144
  322. package/src/checks/valid-static-block-type/index.spec.ts +0 -38
  323. package/src/checks/valid-static-block-type/index.ts +0 -58
  324. package/src/checks/valid-visible-if/index.spec.ts +0 -619
  325. package/src/checks/valid-visible-if/index.ts +0 -184
  326. package/src/checks/valid-visible-if/visible-if-utils.ts +0 -158
  327. package/src/tags/content-for.ts +0 -25
  328. package/src/to-schema.ts +0 -231
  329. package/src/types/schemas/index.ts +0 -5
  330. package/src/types/schemas/preset.ts +0 -52
  331. package/src/types/schemas/section.ts +0 -86
  332. package/src/types/schemas/setting.ts +0 -320
  333. package/src/types/schemas/template.ts +0 -34
  334. package/src/types/schemas/theme-block.ts +0 -34
  335. package/src/types/theme-schemas.ts +0 -80
  336. package/src/utils/block.ts +0 -300
  337. package/src/utils/markup.ts +0 -10
@@ -0,0 +1,259 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { runLiquidCheck, highlightedOffenses } from '../../../test';
3
+ import { LiquidHTMLSyntaxError } from '../index';
4
+
5
+ describe('Module: InvalidTagSyntax', () => {
6
+ describe('render tag', () => {
7
+ it('should report render without quoted template name', async () => {
8
+ const sourceCode = `{% render %}`;
9
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
10
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
11
+ expect(syntaxOffenses).toHaveLength(1);
12
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'render'");
13
+ });
14
+
15
+ it('should not report valid render', async () => {
16
+ const sourceCode = `{% render 'partial' %}`;
17
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
18
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
19
+ expect(syntaxOffenses).toHaveLength(0);
20
+ });
21
+
22
+ it('should not report valid render with arguments', async () => {
23
+ const sourceCode = `{% render 'partial', var1: 'hello', var2: 123 %}`;
24
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
25
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
26
+ expect(syntaxOffenses).toHaveLength(0);
27
+ });
28
+
29
+ it('should highlight the entire invalid render tag', async () => {
30
+ const sourceCode = `Hello {% render %} world`;
31
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
32
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
33
+ const highlights = highlightedOffenses(sourceCode, syntaxOffenses);
34
+ expect(highlights).toContain('{% render %}');
35
+ });
36
+ });
37
+
38
+ describe('function tag', () => {
39
+ it('should report function without = operator', async () => {
40
+ const sourceCode = `{% function res 'path/to/function' %}`;
41
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
42
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
43
+ expect(syntaxOffenses).toHaveLength(1);
44
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'function'");
45
+ });
46
+
47
+ it('should not report valid function', async () => {
48
+ const sourceCode = `{% function res = 'path/to/function' %}`;
49
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
50
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
51
+ expect(syntaxOffenses).toHaveLength(0);
52
+ });
53
+
54
+ it('should not report valid function with arguments', async () => {
55
+ const sourceCode = `{% function res = 'path/to/function', arg1: "hello" %}`;
56
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
57
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
58
+ expect(syntaxOffenses).toHaveLength(0);
59
+ });
60
+ });
61
+
62
+ describe('graphql tag', () => {
63
+ it('should report invalid graphql syntax', async () => {
64
+ const sourceCode = `{% graphql %}`;
65
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
66
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
67
+ expect(syntaxOffenses).toHaveLength(1);
68
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'graphql'");
69
+ });
70
+
71
+ it('should not report valid graphql file-based syntax', async () => {
72
+ const sourceCode = `{% graphql result = 'path/to/query' %}`;
73
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
74
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
75
+ expect(syntaxOffenses).toHaveLength(0);
76
+ });
77
+
78
+ it('should not report graphql with named argument value using a filter', async () => {
79
+ const sourceCode = `{% graphql consumers = 'modules/core/events/consumers', name: name | fetch: "admin_liquid_partials" %}`;
80
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
81
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
82
+ expect(syntaxOffenses).toHaveLength(0);
83
+ });
84
+
85
+ it('should not report graphql with named argument value using chained filters', async () => {
86
+ const sourceCode = `{% graphql consumers = 'modules/core/events/consumers', name: name | fetch: "admin_liquid_partials" | fetch: "results" %}`;
87
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
88
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
89
+ expect(syntaxOffenses).toHaveLength(0);
90
+ });
91
+
92
+ it('should not report graphql with multiple named arguments where one uses a filter', async () => {
93
+ const sourceCode = `{% graphql consumers = 'modules/core/events/consumers', name: name | fetch: "admin_liquid_partials" | fetch: "results", limit: 10 %}`;
94
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
95
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
96
+ expect(syntaxOffenses).toHaveLength(0);
97
+ });
98
+ });
99
+
100
+ describe('include tag', () => {
101
+ it('should report include without template name', async () => {
102
+ const sourceCode = `{% include %}`;
103
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
104
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
105
+ expect(syntaxOffenses).toHaveLength(1);
106
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'include'");
107
+ });
108
+
109
+ it('should not report valid include', async () => {
110
+ const sourceCode = `{% include 'partial' %}`;
111
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
112
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
113
+ expect(syntaxOffenses).toHaveLength(0);
114
+ });
115
+ });
116
+
117
+ describe('platformOS-specific tags', () => {
118
+ it('should not report valid log syntax', async () => {
119
+ const sourceCode = `{% log x %}`;
120
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
121
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
122
+ expect(syntaxOffenses).toHaveLength(0);
123
+ });
124
+
125
+ it('should not report valid export syntax', async () => {
126
+ const sourceCode = `{% export data, namespace: "my_namespace" %}`;
127
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
128
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
129
+ expect(syntaxOffenses).toHaveLength(0);
130
+ });
131
+
132
+ it('should not report valid redirect_to syntax', async () => {
133
+ const sourceCode = `{% redirect_to '/path' %}`;
134
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
135
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
136
+ expect(syntaxOffenses).toHaveLength(0);
137
+ });
138
+
139
+ it('should not report valid print syntax', async () => {
140
+ const sourceCode = `{% print x %}`;
141
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
142
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
143
+ expect(syntaxOffenses).toHaveLength(0);
144
+ });
145
+
146
+ it('should not report valid yield syntax', async () => {
147
+ const sourceCode = `{% yield 'content' %}`;
148
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
149
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
150
+ expect(syntaxOffenses).toHaveLength(0);
151
+ });
152
+ });
153
+
154
+ describe('inside {% liquid %} blocks', () => {
155
+ it('should report invalid render syntax inside liquid block', async () => {
156
+ const sourceCode = `{% liquid
157
+ render
158
+ %}`;
159
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
160
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
161
+ expect(syntaxOffenses).toHaveLength(1);
162
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'render'");
163
+ });
164
+
165
+ it('should not report valid tags inside liquid block', async () => {
166
+ const sourceCode = `{% liquid
167
+ render 'partial'
168
+ function res = 'path/to/function'
169
+ %}`;
170
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
171
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
172
+ expect(syntaxOffenses).toHaveLength(0);
173
+ });
174
+ });
175
+
176
+ describe('should NOT fire on tags with dedicated sub-checks', () => {
177
+ it('should not fire InvalidTagSyntax on assign (has MultipleAssignValues)', async () => {
178
+ const sourceCode = `{% assign x abc %}`;
179
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
180
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
181
+ expect(syntaxOffenses).toHaveLength(0);
182
+ });
183
+
184
+ it('should not fire InvalidTagSyntax on echo (has InvalidEchoValue)', async () => {
185
+ const sourceCode = `{% echo = %}`;
186
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
187
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
188
+ expect(syntaxOffenses).toHaveLength(0);
189
+ });
190
+ });
191
+
192
+ describe('should not report tags without expected markup', () => {
193
+ it('should not report else as invalid syntax', async () => {
194
+ const sourceCode = `{% if true %}a{% else %}b{% endif %}`;
195
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
196
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
197
+ expect(syntaxOffenses).toHaveLength(0);
198
+ });
199
+
200
+ it('should not report break as invalid syntax', async () => {
201
+ const sourceCode = `{% for item in array %}{% break %}{% endfor %}`;
202
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
203
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
204
+ expect(syntaxOffenses).toHaveLength(0);
205
+ });
206
+
207
+ it('should not report continue as invalid syntax', async () => {
208
+ const sourceCode = `{% for item in array %}{% continue %}{% endfor %}`;
209
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
210
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
211
+ expect(syntaxOffenses).toHaveLength(0);
212
+ });
213
+ });
214
+
215
+ describe('tags with whitespace-trimming delimiters', () => {
216
+ it('should report invalid syntax with trimming delimiters', async () => {
217
+ const sourceCode = `{%- render -%}`;
218
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
219
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
220
+ expect(syntaxOffenses).toHaveLength(1);
221
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'render'");
222
+ });
223
+
224
+ it('should not report valid syntax with trimming delimiters', async () => {
225
+ const sourceCode = `{%- render 'partial' -%}`;
226
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
227
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
228
+ expect(syntaxOffenses).toHaveLength(0);
229
+ });
230
+ });
231
+
232
+ describe('docset syntax hint', () => {
233
+ it('should include syntax hint from docset when available', async () => {
234
+ const sourceCode = `{% render %}`;
235
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode, 'file.liquid', {
236
+ platformosDocset: {
237
+ async filters() {
238
+ return [];
239
+ },
240
+ async objects() {
241
+ return [];
242
+ },
243
+ async liquidDrops() {
244
+ return [];
245
+ },
246
+ async tags() {
247
+ return [{ name: 'render', syntax: "{% render 'partial' %}" }];
248
+ },
249
+ async graphQL() {
250
+ return null;
251
+ },
252
+ },
253
+ });
254
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
255
+ expect(syntaxOffenses).toHaveLength(1);
256
+ expect(syntaxOffenses[0].message).toContain("Expected syntax: {% render 'partial' %}");
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,89 @@
1
+ import { LiquidTag, NamedTags, TAGS_WITHOUT_MARKUP } from '@platformos/liquid-html-parser';
2
+ import { Problem, SourceCodeType, TagEntry } from '../../..';
3
+
4
+ /**
5
+ * Tags that use no markup at all — they are valid as `{% else %}`, `{% break %}`, etc.
6
+ * When their markup is a string, it's always '' (empty), so they should not trigger this check.
7
+ */
8
+ const TAGS_WITH_NO_EXPECTED_MARKUP = new Set<string>(TAGS_WITHOUT_MARKUP);
9
+
10
+ /**
11
+ * Tags that have dedicated sub-checks handling their string-markup cases with
12
+ * more specific error messages and autofixes. This check should NOT fire on these
13
+ * to avoid double-reporting or overriding their nuanced decisions.
14
+ *
15
+ * - assign → MultipleAssignValues, InvalidFilterName, InvalidPipeSyntax
16
+ * - echo → InvalidEchoValue, InvalidFilterName, InvalidPipeSyntax
17
+ * - if/elsif/unless → InvalidConditionalNode, InvalidConditionalNodeParenthesis
18
+ * - for/tablerow → InvalidLoopRange, InvalidLoopArguments
19
+ */
20
+ const TAGS_WITH_DEDICATED_CHECKS = new Set<string>([
21
+ NamedTags.assign,
22
+ NamedTags.echo,
23
+ NamedTags.if,
24
+ NamedTags.elsif,
25
+ NamedTags.unless,
26
+ NamedTags.for,
27
+ NamedTags.tablerow,
28
+ NamedTags.when,
29
+ ]);
30
+
31
+ /**
32
+ * All tag names in the NamedTags enum — these have specific grammar rules for their markup.
33
+ * If a NamedTag's markup is a string (instead of a parsed object), it means the strict grammar
34
+ * rule failed and the tag fell through to the base case.
35
+ */
36
+ const NAMED_TAGS = new Set<string>(Object.values(NamedTags));
37
+
38
+ /**
39
+ * Detects known tags whose markup couldn't be parsed by the grammar.
40
+ *
41
+ * When the tolerant parser encounters a known tag name (e.g. "render") but can't parse
42
+ * the markup with the strict grammar rule, it falls back to the base case and stores
43
+ * the markup as a raw string. This function detects that situation.
44
+ *
45
+ * This check only applies to tags that DON'T have more specific sub-checks.
46
+ * Tags like assign, echo, if, for etc. have dedicated checks that provide
47
+ * better error messages and autofixes for their specific syntax patterns.
48
+ *
49
+ * Examples:
50
+ * {% graphql %} → name: 'graphql', markup: '' (string — invalid)
51
+ * {% render %} → name: 'render', markup: '' (string — invalid)
52
+ * {% function res 'path' %} → name: 'function', markup: "res 'path'" (string — invalid)
53
+ */
54
+ export function detectInvalidTagSyntax(
55
+ node: LiquidTag,
56
+ tags: TagEntry[] = [],
57
+ ): Problem<SourceCodeType.LiquidHtml> | undefined {
58
+ const tagName = node.name;
59
+
60
+ // Only check tags known to the grammar with specific markup rules
61
+ if (!NAMED_TAGS.has(tagName)) {
62
+ return;
63
+ }
64
+
65
+ // Tags without expected markup (else, break, continue, etc.) always have string markup
66
+ if (TAGS_WITH_NO_EXPECTED_MARKUP.has(tagName)) {
67
+ return;
68
+ }
69
+
70
+ // Skip tags that have dedicated sub-checks with more specific error handling
71
+ if (TAGS_WITH_DEDICATED_CHECKS.has(tagName)) {
72
+ return;
73
+ }
74
+
75
+ // If markup is not a string, it was parsed successfully — no error
76
+ if (typeof node.markup !== 'string') {
77
+ return;
78
+ }
79
+
80
+ // Build a helpful hint from the docset if available
81
+ const tagEntry = tags.find((t) => t.name === tagName);
82
+ const syntaxHint = tagEntry?.syntax ? ` Expected syntax: ${tagEntry.syntax}` : '';
83
+
84
+ return {
85
+ message: `Invalid syntax for tag '${tagName}'${syntaxHint}`,
86
+ startIndex: node.position.start,
87
+ endIndex: node.position.end,
88
+ };
89
+ }
@@ -0,0 +1,293 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { runLiquidCheck, highlightedOffenses } from '../../../test';
3
+ import { LiquidHTMLSyntaxError } from '../index';
4
+
5
+ describe('Module: UnknownTag', () => {
6
+ describe('standalone unknown tags', () => {
7
+ it('should report an unknown inline tag', async () => {
8
+ const sourceCode = `{% dsjkds %}`;
9
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
10
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
11
+ expect(unknownTagOffenses).toHaveLength(1);
12
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'dsjkds'");
13
+ });
14
+
15
+ it('should report an unknown tag with markup', async () => {
16
+ const sourceCode = `{% foobar some_arg %}`;
17
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
18
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
19
+ expect(unknownTagOffenses).toHaveLength(1);
20
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'foobar'");
21
+ });
22
+
23
+ it('should highlight the entire unknown tag', async () => {
24
+ const sourceCode = `Hello {% unknown_tag %} world`;
25
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
26
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
27
+ const highlights = highlightedOffenses(sourceCode, unknownTagOffenses);
28
+ expect(highlights).toContain('{% unknown_tag %}');
29
+ });
30
+
31
+ it('should report multiple unknown tags', async () => {
32
+ const sourceCode = `{% foo %} {% bar %} {% baz %}`;
33
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
34
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
35
+ expect(unknownTagOffenses).toHaveLength(3);
36
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'foo'");
37
+ expect(unknownTagOffenses[1].message).toBe("Unknown tag 'bar'");
38
+ expect(unknownTagOffenses[2].message).toBe("Unknown tag 'baz'");
39
+ });
40
+ });
41
+
42
+ describe('unknown tags inside {% liquid %} blocks', () => {
43
+ it('should report an unknown tag inside a liquid block', async () => {
44
+ const sourceCode = `{% liquid
45
+ assign x = "abc"
46
+ dasjkdjkas
47
+ %}`;
48
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
49
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
50
+ expect(unknownTagOffenses).toHaveLength(1);
51
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'dasjkdjkas'");
52
+ });
53
+
54
+ it('should report multiple unknown tags inside a liquid block', async () => {
55
+ const sourceCode = `{% liquid
56
+ assign x = "abc"
57
+ foo
58
+ echo x
59
+ bar
60
+ %}`;
61
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
62
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
63
+ expect(unknownTagOffenses).toHaveLength(2);
64
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'foo'");
65
+ expect(unknownTagOffenses[1].message).toBe("Unknown tag 'bar'");
66
+ });
67
+
68
+ it('should not report valid tags inside a liquid block', async () => {
69
+ const sourceCode = `{% liquid
70
+ assign x = "hello"
71
+ echo x
72
+ assign y = x | upcase
73
+ %}`;
74
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
75
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
76
+ expect(unknownTagOffenses).toHaveLength(0);
77
+ });
78
+ });
79
+
80
+ describe('should NOT report known tags', () => {
81
+ it('should not report standard liquid tags', async () => {
82
+ const validTags = [
83
+ `{% assign x = "hello" %}`,
84
+ `{% echo "hello" %}`,
85
+ `{% increment counter %}`,
86
+ `{% decrement counter %}`,
87
+ `{% cycle "a", "b", "c" %}`,
88
+ `{% break %}`,
89
+ `{% continue %}`,
90
+ `{% layout 'application' %}`,
91
+ `{% render 'partial' %}`,
92
+ `{% include 'partial' %}`,
93
+ ];
94
+
95
+ for (const sourceCode of validTags) {
96
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
97
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
98
+ expect(
99
+ unknownTagOffenses,
100
+ `Expected no unknown tag offense for: ${sourceCode}`,
101
+ ).toHaveLength(0);
102
+ }
103
+ });
104
+
105
+ it('should not report block tags', async () => {
106
+ const validBlocks = [
107
+ `{% if true %}hello{% endif %}`,
108
+ `{% unless false %}hello{% endunless %}`,
109
+ `{% for item in array %}{{ item }}{% endfor %}`,
110
+ `{% capture var %}hello{% endcapture %}`,
111
+ `{% case x %}{% when 1 %}one{% endcase %}`,
112
+ ];
113
+
114
+ for (const sourceCode of validBlocks) {
115
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
116
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
117
+ expect(
118
+ unknownTagOffenses,
119
+ `Expected no unknown tag offense for: ${sourceCode}`,
120
+ ).toHaveLength(0);
121
+ }
122
+ });
123
+
124
+ it('should not report platformOS-specific tags', async () => {
125
+ const validTags = [
126
+ `{% log x %}`,
127
+ `{% print x %}`,
128
+ `{% yield 'content' %}`,
129
+ `{% redirect_to '/path' %}`,
130
+ `{% export x, namespace: "ns" %}`,
131
+ `{% return x %}`,
132
+ `{% response_status 200 %}`,
133
+ `{% response_headers 'Content-Type': 'text/html' %}`,
134
+ `{% sign_in user %}`,
135
+ `{% spam_protection "recaptcha_v2" %}`,
136
+ `{% theme_render_rc 'rc' %}`,
137
+ ];
138
+
139
+ for (const sourceCode of validTags) {
140
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
141
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
142
+ expect(
143
+ unknownTagOffenses,
144
+ `Expected no unknown tag offense for: ${sourceCode}`,
145
+ ).toHaveLength(0);
146
+ }
147
+ });
148
+
149
+ it('should not report platformOS block tags', async () => {
150
+ const validBlocks = [
151
+ `{% cache 'key' %}hello{% endcache %}`,
152
+ `{% parse_json var %}{}{% endparse_json %}`,
153
+ `{% try %}hello{% catch err %}{{ err }}{% endtry %}`,
154
+ `{% content_for 'pagetitle' %}<title>Hello</title>{% endcontent_for %}`,
155
+ `{% background source_name: 'my_task' %}echo "hello"{% endbackground %}`,
156
+ ];
157
+
158
+ for (const sourceCode of validBlocks) {
159
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
160
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
161
+ expect(
162
+ unknownTagOffenses,
163
+ `Expected no unknown tag offense for: ${sourceCode}`,
164
+ ).toHaveLength(0);
165
+ }
166
+ });
167
+
168
+ it('should not report raw tags', async () => {
169
+ const sourceCode = `{% raw %}{{ not liquid }}{% endraw %}`;
170
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
171
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
172
+ expect(unknownTagOffenses).toHaveLength(0);
173
+ });
174
+
175
+ it('should not report comment tags', async () => {
176
+ const sourceCode = `{% comment %}this is a comment{% endcomment %}`;
177
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
178
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
179
+ expect(unknownTagOffenses).toHaveLength(0);
180
+ });
181
+
182
+ it('should not report inline comment tags', async () => {
183
+ const sourceCode = `{% # this is an inline comment %}`;
184
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
185
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
186
+ expect(unknownTagOffenses).toHaveLength(0);
187
+ });
188
+
189
+ it('should not report else/elsif tags', async () => {
190
+ const sourceCode = `{% if true %}a{% elsif false %}b{% else %}c{% endif %}`;
191
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
192
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
193
+ expect(unknownTagOffenses).toHaveLength(0);
194
+ });
195
+ });
196
+
197
+ describe('tags known via docset', () => {
198
+ it('should not report tags from the docset', async () => {
199
+ const sourceCode = `{% custom_docset_tag %}`;
200
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode, 'file.liquid', {
201
+ platformosDocset: {
202
+ async filters() {
203
+ return [];
204
+ },
205
+ async objects() {
206
+ return [];
207
+ },
208
+ async liquidDrops() {
209
+ return [];
210
+ },
211
+ async tags() {
212
+ return [{ name: 'custom_docset_tag' }];
213
+ },
214
+ async graphQL() {
215
+ return null;
216
+ },
217
+ },
218
+ });
219
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
220
+ expect(unknownTagOffenses).toHaveLength(0);
221
+ });
222
+ });
223
+
224
+ describe('mixed valid and unknown tags', () => {
225
+ it('should only report the unknown tags in mixed content', async () => {
226
+ const sourceCode = `
227
+ {% assign x = "hello" %}
228
+ {% unknown_one %}
229
+ {% if true %}
230
+ {% bogus_tag %}
231
+ {% endif %}
232
+ `;
233
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
234
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
235
+ expect(unknownTagOffenses).toHaveLength(2);
236
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'unknown_one'");
237
+ expect(unknownTagOffenses[1].message).toBe("Unknown tag 'bogus_tag'");
238
+ });
239
+ });
240
+
241
+ describe('real-world file patterns', () => {
242
+ it('should catch unknown tags in a real platformOS page with liquid block and standalone tag', async () => {
243
+ const sourceCode = `---
244
+ method: post
245
+ slug: users
246
+ layout: 'modules/community/blank'
247
+ ---
248
+
249
+ {% liquid
250
+ function current_profile = 'modules/user/helpers/current_profile'
251
+
252
+ include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'users.register', redirect_url: "/"
253
+
254
+ function object = 'modules/user/commands/user/create', first_name: params.first_name
255
+
256
+ dsk
257
+
258
+ if object.valid
259
+ function _ = 'modules/user/commands/session/create', user_id: object.id
260
+ include 'modules/core/helpers/redirect_to', url: '/onboarding'
261
+ else
262
+ assign values = object | default: null
263
+ render 'modules/user/users/new', errors: object.errors, values: values
264
+ endif
265
+ %}
266
+
267
+ {% jakdsajk %}`;
268
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
269
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
270
+ expect(unknownTagOffenses).toHaveLength(2);
271
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'dsk'");
272
+ expect(unknownTagOffenses[1].message).toBe("Unknown tag 'jakdsajk'");
273
+ });
274
+ });
275
+
276
+ describe('edge cases', () => {
277
+ it('should report unknown tags with underscores', async () => {
278
+ const sourceCode = `{% my_custom_tag %}`;
279
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
280
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
281
+ expect(unknownTagOffenses).toHaveLength(1);
282
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'my_custom_tag'");
283
+ });
284
+
285
+ it('should report unknown tags with whitespace-trimming delimiters', async () => {
286
+ const sourceCode = `{%- unknown_tag -%}`;
287
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
288
+ const unknownTagOffenses = offenses.filter((o) => o.message.includes('Unknown tag'));
289
+ expect(unknownTagOffenses).toHaveLength(1);
290
+ expect(unknownTagOffenses[0].message).toBe("Unknown tag 'unknown_tag'");
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,43 @@
1
+ import {
2
+ LiquidTag,
3
+ NamedTags,
4
+ TAGS_WITHOUT_MARKUP,
5
+ BLOCKS,
6
+ RAW_TAGS,
7
+ } from '@platformos/liquid-html-parser';
8
+ import { Problem, SourceCodeType, TagEntry } from '../../..';
9
+
10
+ /**
11
+ * All tag names known to the grammar (NamedTags enum + TAGS_WITHOUT_MARKUP + BLOCKS + RAW_TAGS).
12
+ * These are tags that the parser recognizes with specific grammar rules.
13
+ */
14
+ const GRAMMAR_KNOWN_TAGS = new Set<string>([
15
+ ...Object.values(NamedTags),
16
+ ...TAGS_WITHOUT_MARKUP,
17
+ ...BLOCKS,
18
+ ...RAW_TAGS,
19
+ '#', // inline comment: {% # this is a comment %}
20
+ ]);
21
+
22
+ export function detectUnknownTag(
23
+ node: LiquidTag,
24
+ tags: TagEntry[] = [],
25
+ ): Problem<SourceCodeType.LiquidHtml> | undefined {
26
+ const tagName = node.name;
27
+
28
+ // If the tag is known to the grammar, it's not unknown
29
+ if (GRAMMAR_KNOWN_TAGS.has(tagName)) {
30
+ return;
31
+ }
32
+
33
+ // If the tag is known to the docset, it's not unknown
34
+ if (tags.some((tag) => tag.name === tagName)) {
35
+ return;
36
+ }
37
+
38
+ return {
39
+ message: `Unknown tag '${tagName}'`,
40
+ startIndex: node.position.start,
41
+ endIndex: node.position.end,
42
+ };
43
+ }