Repository navigation
Populate ScriptTokenStream and propagate precise token bounds during AST construction - #35
Conversation
Co-authored-by: fupacat <595487+fupacat@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The bound-propagation changes contain correctness bugs (a firstTokenIndex == 0 sentinel that corrupts leading fragments, and per-fragment stream copies that are partial and O(fragments × tokens)) in generated-parser code whose runtime behavior cannot be fully verified here.
2 open findings
What changed in this PR
This PR makes TSqlParser.parse(sql) emit usable source-span metadata so downstream analyzers (e.g. SonarQube highlighting, per issue #34) can map AST fragments back to exact token ranges. It captures the full token stream in the parser's input filter, exposes helpers to resolve a token's stream index, and rewrites the generated parser's UpdateTokenInfo/AddAndUpdateTokenInfo helpers plus the TSqlFragment base merge logic to attach the stream and propagate firstTokenIndex/lastTokenIndex bounds through construction.
Changes:
TSqlWhitespaceTokenFilternow records every emitted token in parse order and maintains a token→index map, with accessors for the generated parser.- The generated helper methods attach the script token stream onto fragments, resolve token indices, and propagate parent bounds when adding children;
TSqlFragment.UpdateTokenInfo(Object)now merges bounds from another fragment. - A new facade test asserts the root stream is populated and fragment bounds are non-default.
| File | Description |
|---|---|
| tsql-scriptdom-core/src/main/java/io/github/fupacat/TSqlWhitespaceTokenFilter.java | Records emitted tokens into scriptTokenStream, maps token→index, and exposes _getScriptTokenStream/_getTokenIndex. |
| scripts/generate_parser.py | Rewrites generated helpers to attach the stream and propagate bounds; removes the no-op UpdateTokenInfo. |
| tsql-scriptdom-astgen/src/main/java/io/github/fupacat/AstCodeGenerator.java | Adds fragment-to-fragment bound merge in TSqlFragment.UpdateTokenInfo(Object). |
| tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java | Adds a regression test for populated stream and propagated token bounds. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ' public void AddAndUpdateTokenInfo(Object... args) { if (args.length >= 3 && args[1] instanceof java.util.List list && args[2] != null) { if (args[2] instanceof java.util.Collection<?> c) { list.addAll(c); } else { list.add(args[2]); } if (args[0] instanceof SchemaObjectName son) { int count = son.Identifiers.size(); if (count > 0) son.BaseIdentifier = son.Identifiers.get(count - 1); if (count > 1) son.SchemaIdentifier = son.Identifiers.get(count - 2); if (count > 2) son.DatabaseIdentifier = son.Identifiers.get(count - 3); if (count > 3) son.ServerIdentifier = son.Identifiers.get(count - 4); } } }', | ||
| ' public void AddAndUpdateTokenInfo(Object... args) { if (args.length >= 3 && args[1] instanceof java.util.List list && args[2] != null) { if (args[2] instanceof java.util.Collection<?> c) { list.addAll(c); for (Object item : c) { UpdateTokenInfo(args[0], item); } } else { list.add(args[2]); UpdateTokenInfo(args[0], args[2]); } if (args[0] instanceof SchemaObjectName son) { int count = son.Identifiers.size(); if (count > 0) son.BaseIdentifier = son.Identifiers.get(count - 1); if (count > 1) son.SchemaIdentifier = son.Identifiers.get(count - 2); if (count > 2) son.DatabaseIdentifier = son.Identifiers.get(count - 3); if (count > 3) son.ServerIdentifier = son.Identifiers.get(count - 4); } } }', | ||
| ' public int ResolveTokenIndex(Object tokenLike) { if (_tokenSource == null || tokenLike == null) return -1; if (tokenLike instanceof Token tok) { return _tokenSource._getTokenIndex(tok); } return -1; }', | ||
| ' public void AttachScriptTokenStream(Object fragment) { if (_tokenSource == null || !(fragment instanceof TSqlFragment f) || f.ScriptTokenStream == null) return; if (f.ScriptTokenStream.isEmpty()) { f.ScriptTokenStream.addAll(_tokenSource._getScriptTokenStream()); } }', |
| .addModifiers(javax.lang.model.element.Modifier.PUBLIC) | ||
| .addParameter(Object.class, "other") | ||
| .beginControlFlow("if (other instanceof TSqlFragment fragment)") | ||
| .beginControlFlow("if (this.firstTokenIndex == 0 || (fragment.firstTokenIndex > 0 && fragment.firstTokenIndex < this.firstTokenIndex))") |
There was a problem hiding this comment.
🔵 Needs a closer look
Per-fragment partial token-stream snapshots leave child/root ScriptTokenStream incomplete while token indices point beyond them, breaking the intended downstream span lookups for multi-statement/multi-batch scripts.
2 open findings
🧠 Review effort: Balanced
…ript-token-stream # Conflicts: # tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java
There was a problem hiding this comment.
🟡 Changes recommended
The 0-as-sentinel bound logic and the per-fragment stream snapshot produce incorrect firstTokenIndex/ScriptTokenStream values for fragments anchored at token 0 and for multi-batch scripts, undermining the PR's core goal.
4 open findings
🧠 Review effort: Balanced
| ' public void AddAndUpdateTokenInfo(Object... args) { if (args.length >= 3 && args[1] instanceof java.util.List list && args[2] != null) { if (args[2] instanceof java.util.Collection<?> c) { list.addAll(c); for (Object item : c) { UpdateTokenInfo(args[0], item); } } else { list.add(args[2]); UpdateTokenInfo(args[0], args[2]); } if (args[0] instanceof SchemaObjectName son) { int count = son.Identifiers.size(); if (count > 0) son.BaseIdentifier = son.Identifiers.get(count - 1); if (count > 1) son.SchemaIdentifier = son.Identifiers.get(count - 2); if (count > 2) son.DatabaseIdentifier = son.Identifiers.get(count - 3); if (count > 3) son.ServerIdentifier = son.Identifiers.get(count - 4); } } }', | ||
| ' public int ResolveTokenIndex(Object tokenLike) { if (_tokenSource == null || tokenLike == null) return -1; if (tokenLike instanceof Token tok) { return _tokenSource._getTokenIndex(tok); } return -1; }', | ||
| ' public void AttachScriptTokenStream(Object fragment) { if (_tokenSource == null || !(fragment instanceof TSqlFragment f) || f.ScriptTokenStream == null) return; if (f.ScriptTokenStream.isEmpty()) { f.ScriptTokenStream.addAll(_tokenSource._getScriptTokenStream()); } }', | ||
| ' public void UpdateTokenInfo(Object... args) { if (args == null || args.length == 0 || !(args[0] instanceof TSqlFragment fragment)) return; AttachScriptTokenStream(fragment); if (args.length >= 3 && args[1] instanceof Number first && args[2] instanceof Number last) { fragment.UpdateTokenInfo(first.intValue(), last.intValue()); return; } if (args.length >= 2 && args[1] instanceof TSqlFragment child) { fragment.UpdateTokenInfo(child); return; } if (args.length >= 2) { int idx = ResolveTokenIndex(args[1]); if (idx >= 0) { if (fragment.firstTokenIndex == 0 || idx < fragment.firstTokenIndex) { fragment.firstTokenIndex = idx; } if (idx > fragment.lastTokenIndex) { fragment.lastTokenIndex = idx; } } } }', |
| assertThat(result.isSuccess()).isTrue(); | ||
| } | ||
|
|
||
| @Test |



TSqlParser.parse(sql)was producing AST fragments without usable token-range metadata:TSqlScript.ScriptTokenStreamremained empty and many child fragments kept default token bounds (firstTokenIndex/lastTokenIndex), which prevented downstream analyzers from computing exact source spans.This change wires token-stream retention and bound propagation through parser construction paths so fragment ranges map to real token positions.
Token stream capture in parser input filter
TSqlWhitespaceTokenFilternow records emitted tokens in parse order and tracks token-object → stream-index mapping.Generated parser helper: real token-bound propagation
scripts/generate_parser.py) so generated helper methods (UpdateTokenInfo,AddAndUpdateTokenInfo) now:AST base behavior: fragment-to-fragment bound merge
AstCodeGenerator) soTSqlFragment.UpdateTokenInfo(Object)merges bounds when passed anotherTSqlFragmentinstead of no-op behavior.Regression coverage for range metadata