Repository navigation
fix(gen.treesitter-ng): convert trees in one cursor pass with UTF-16 offsets - #489
Merged
Merged
Conversation
…offsets Convert the tree-sitter tree with a single TSTreeCursor traversal that keeps the types of the ancestors for the rules, instead of a recursive traversal that asks tree-sitter for the parent of each node (ts_node_parent walks down from the root) and makes several JNI calls per node. Positions and lengths are now UTF-16 offsets computed from the UTF-8 byte offsets of tree-sitter, so nodes after non-ASCII characters on the same line get correct offsets. Labels are the source text of the nodes, so multi-line nodes keep all their lines. The content is parsed as UTF-8 bytes, because TSParser.parseString drops U+FEFF characters and shifts the following nodes. The rules of each language are precomputed once. Refs GumTreeDiff#485 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #485
This PR fixes the converter problems from §1 of #485 in
AbstractTreeSitterNgGeneratorand works around the two tree-sitter-ng binding problems from §2 on the GumTree side. The binding itself can only be fixed in bonede/tree-sitter-ng.Changes
Performance
TSTreeCursortraversal. The types of the ancestors are kept in a list for the ancestor rules (a b cinrules.yml). The old recursive traversal calledgetParent()for every node and every rule set.ts_node_parentwalks down from the root each time, so the cost grew with depth × tree size. It also made several JNI calls per node (getChild(i),getType,getStartPoint, …).ignored,label_ignored,flattened,aliased) are precomputed once and cached: direct types go in a hash set, plus the maximum ancestor depth. Before, every rule string was split again for every node.TSParser,TSTreeandTSTreeCursorare closed with try-with-resources.Correctness
pos/length(§1).content.substring(pos, end). Before, multi-line leaves and flattened nodes (Python triple-quoted strings, template literals, …) were labelled with only part of their text.TSParser.parse(byte[], …, TSInputEncodingUTF8)instead ofparseString. The binding'sConvertUTF.csilently drops U+FEFF, U+FFFE and U+FFFF, which shifted every following node by 3 bytes (§2.1).API note: the protected static helpers
tsNode2GumTree,getLabelandcalculateOffsetwere removed. No subclass in the repository uses them.matchNodeOrAncestorTypesis kept, and now only callsgetParent()when no direct rule matched.Tests
New tests in
AbstractTreeSitterNgGeneratorTest. Six of them fail onmainand pass with this PR:testMultiByteOffsetOnSameLine,testSurrogatePairOffsetOnSameLine: offsets afteré/🐍on the same line.testMultiLineLabel,testMultiLineLabelCrlf: full multi-line labels, with LF and CRLF.testByteOrderMark: positions after a leading BOM.testFlattenedAndAncestorRules: flattening plus ignored/ancestor rules (default_parameter =) still apply.Each test also checks that every leaf label equals its source range.
./gradlew :gen.treesitter-ng:check :gen.kotlin:checkpasses.Experiments
Corpus: all source files of the test datasets of RefactoringMiner, from
src/test/resources/oracle/commits. These are the before/after files of the commits in its Python, Kotlin, TypeScript/JavaScript and C++ refactoring oracles. 5,467 files, 12.7M GumTree nodes.Setup:
main.TSParser.parseStringonly, the lower bound.GumTreeDiff/gumtree6e9a863..cpp/.cc/.c/.h/.hpp).js/.jsx)The round-to-round spread is below 1.5% in all configurations. Python and Kotlin gain the most because their
rules.ymlsections use ancestor rules, which triggered the quadraticgetParent()walks. In the other grammars the remaining cost is mostly the JNI calls for each node.Equivalence check: each tree was dumped in preorder as (depth, type, pos, length, label) with both versions and compared node by node:
Tree structure and types are identical for all 5,467 files: same shape, same types, same node counts as listed above.
pos/lengthdiffer only in files that contain non-ASCII characters or a BOM. In every such case the new range is the correct one:With this PR, every leaf label equals
content.substring(pos, pos + length), and every range is within the source. Onmain, multi-line labels were shifted text windows. For example, a Kotlin KDoc was labelled" \n * Get a Task with the given ID. Will return null i…"instead of"/**\n * Get a Task with the given ID. …". In a Python file with a BOM, the label ofimportwas"\ufeffimp".Not in this PR (tree-sitter-ng)
These belong in bonede/tree-sitter-ng:
ConvertUTF.cskips0xFEFF,0xFFFEand0xFFFFwhile converting the string, soparseStringparses a different input than it was given. This PR avoids it by passing UTF-8 bytes. The fix in the binding needs its natives rebuilt.TSNodeaccessor first callsisNull(), which is a native call (ts_node_is_null), so each accessor costs two JNI calls.isNull()could beidPtr == 0in Java. This PR keeps the number of accessor calls per node small, but the binding fix would help every user.🤖 Generated with Claude Code