debug-print.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. // from https://gist.github.com/msteen/e4828fbf25d6efef73576fc43ac479d2
  2. // https://discuss.codemirror.net/t/whats-the-best-to-test-and-debug-grammars/2542/5
  3. // MIT License
  4. //
  5. // Copyright (c) 2021 Matthijs Steen
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in all
  15. // copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. // SOFTWARE.
  24. import { Text } from '@codemirror/state'
  25. import { Input, NodeType, SyntaxNode, Tree, TreeCursor } from '@lezer/common'
  26. class StringInput implements Input {
  27. private input: string
  28. constructor(input: string) {
  29. this.input = input
  30. }
  31. get length() {
  32. return this.input.length
  33. }
  34. chunk(from: number): string {
  35. return this.input.slice(from)
  36. }
  37. lineChunks = false
  38. read(from: number, to: number): string {
  39. return this.input.slice(from, to)
  40. }
  41. }
  42. export function sliceType(
  43. cursor: TreeCursor,
  44. input: Input,
  45. type: number
  46. ): string | null {
  47. if (cursor.type.id === type) {
  48. const s = input.read(cursor.from, cursor.to)
  49. cursor.nextSibling()
  50. return s
  51. }
  52. return null
  53. }
  54. export function isType(cursor: TreeCursor, type: number): boolean {
  55. const cond = cursor.type.id === type
  56. if (cond) cursor.nextSibling()
  57. return cond
  58. }
  59. export type CursorNode = {
  60. type: NodeType
  61. from: number
  62. to: number
  63. isLeaf: boolean
  64. }
  65. function cursorNode(
  66. { type, from, to }: TreeCursor,
  67. isLeaf = false
  68. ): CursorNode {
  69. return { type, from, to, isLeaf }
  70. }
  71. export type TreeTraversal = {
  72. beforeEnter?: (cursor: TreeCursor) => void
  73. onEnter: (node: CursorNode) => false | void
  74. onLeave?: (node: CursorNode) => false | void
  75. }
  76. type TreeTraversalOptions = {
  77. from?: number
  78. to?: number
  79. includeParents?: boolean
  80. } & TreeTraversal
  81. export function traverseTree(
  82. cursor: TreeCursor | Tree | SyntaxNode,
  83. {
  84. from = -Infinity,
  85. to = Infinity,
  86. includeParents = false,
  87. beforeEnter,
  88. onEnter,
  89. onLeave,
  90. }: TreeTraversalOptions
  91. ): void {
  92. if (!(cursor instanceof TreeCursor))
  93. cursor = cursor instanceof Tree ? cursor.cursor() : cursor.cursor()
  94. for (;;) {
  95. let node = cursorNode(cursor)
  96. let leave = false
  97. if (node.from <= to && node.to >= from) {
  98. const enter =
  99. !node.type.isAnonymous &&
  100. (includeParents || (node.from >= from && node.to <= to))
  101. if (enter && beforeEnter) beforeEnter(cursor)
  102. node.isLeaf = !cursor.firstChild()
  103. if (enter) {
  104. leave = true
  105. if (onEnter(node) === false) return
  106. }
  107. if (!node.isLeaf) continue
  108. }
  109. for (;;) {
  110. node = cursorNode(cursor, node.isLeaf)
  111. if (leave && onLeave) if (onLeave(node) === false) return
  112. leave = cursor.type.isAnonymous
  113. node.isLeaf = false
  114. if (cursor.nextSibling()) break
  115. if (!cursor.parent()) return
  116. leave = true
  117. }
  118. }
  119. }
  120. function isChildOf(child: CursorNode, parent: CursorNode): boolean {
  121. return (
  122. child.from >= parent.from &&
  123. child.from <= parent.to &&
  124. child.to <= parent.to &&
  125. child.to >= parent.from
  126. )
  127. }
  128. export function validatorTraversal(
  129. input: Input | string,
  130. { fullMatch = true }: { fullMatch?: boolean } = {}
  131. ) {
  132. if (typeof input === 'string') input = new StringInput(input)
  133. const state = {
  134. valid: true,
  135. parentNodes: [] as CursorNode[],
  136. lastLeafTo: 0,
  137. }
  138. return {
  139. state,
  140. traversal: {
  141. onEnter(node) {
  142. state.valid = true
  143. if (!node.isLeaf) state.parentNodes.unshift(node)
  144. if (node.from > node.to || node.from < state.lastLeafTo) {
  145. state.valid = false
  146. } else if (node.isLeaf) {
  147. if (
  148. state.parentNodes.length &&
  149. !isChildOf(node, state.parentNodes[0])
  150. )
  151. state.valid = false
  152. state.lastLeafTo = node.to
  153. } else {
  154. if (state.parentNodes.length) {
  155. if (!isChildOf(node, state.parentNodes[0])) state.valid = false
  156. } else if (
  157. fullMatch &&
  158. (node.from !== 0 || node.to !== input.length)
  159. ) {
  160. state.valid = false
  161. }
  162. }
  163. },
  164. onLeave(node) {
  165. if (!node.isLeaf) state.parentNodes.shift()
  166. },
  167. } as TreeTraversal,
  168. }
  169. }
  170. export function validateTree(
  171. tree: TreeCursor | Tree | SyntaxNode,
  172. input: Input | string,
  173. options?: { fullMatch?: boolean }
  174. ): boolean {
  175. const { state, traversal } = validatorTraversal(input, options)
  176. traverseTree(tree, traversal)
  177. return state.valid
  178. }
  179. function colorize(value: any): string {
  180. return '' + value
  181. }
  182. type PrintTreeOptions = {
  183. from?: number
  184. to?: number
  185. start?: number
  186. includeParents?: boolean
  187. }
  188. export function printTree(
  189. cursor: TreeCursor | Tree | SyntaxNode,
  190. input: Input | string,
  191. { from, to, start = 0, includeParents }: PrintTreeOptions = {}
  192. ): string {
  193. const inp = typeof input === 'string' ? new StringInput(input) : input
  194. const text = Text.of(inp.read(0, inp.length).split('\n'))
  195. const state = {
  196. output: '',
  197. prefixes: [] as string[],
  198. hasNextSibling: false,
  199. }
  200. const validator = validatorTraversal(inp)
  201. traverseTree(cursor, {
  202. from,
  203. to,
  204. includeParents,
  205. beforeEnter(cursor) {
  206. state.hasNextSibling = cursor.nextSibling() && cursor.prevSibling()
  207. },
  208. onEnter(node) {
  209. validator.traversal.onEnter(node)
  210. const isTop = state.output === ''
  211. const hasPrefix = !isTop || node.from > 0
  212. if (hasPrefix) {
  213. state.output += (!isTop ? '\n' : '') + state.prefixes.join('')
  214. if (state.hasNextSibling) {
  215. state.output += ' ├─ '
  216. state.prefixes.push(' │ ')
  217. } else {
  218. state.output += ' └─ '
  219. state.prefixes.push(' ')
  220. }
  221. }
  222. const hasRange = node.from !== node.to
  223. state.output +=
  224. (node.type.isError || !validator.state.valid
  225. ? colorize('ERROR ' + node.type.name)
  226. : node.type.name) +
  227. ' ' +
  228. (hasRange
  229. ? '[' +
  230. colorize(locAt(text, start + node.from)) +
  231. '..' +
  232. colorize(locAt(text, start + node.to)) +
  233. ']'
  234. : colorize(locAt(text, start + node.from)))
  235. if (hasRange && node.isLeaf) {
  236. state.output +=
  237. ': ' + colorize(JSON.stringify(inp.read(node.from, node.to)))
  238. }
  239. },
  240. onLeave(node) {
  241. if (validator.traversal.onLeave) {
  242. validator.traversal.onLeave(node)
  243. }
  244. state.prefixes.pop()
  245. },
  246. })
  247. return state.output
  248. }
  249. function locAt(text: Text, pos: number): string {
  250. const line = text.lineAt(pos)
  251. return line.number + ':' + (pos - line.from)
  252. }
  253. export function logTree(
  254. tree: TreeCursor | Tree | SyntaxNode,
  255. input: string,
  256. options?: PrintTreeOptions
  257. ): void {
  258. console.log(printTree(tree, input, options))
  259. }