fix(udl scanner): lex Language = python bodies with string awareness - #50
fix(udl scanner): lex Language = python bodies with string awareness#50isc-tdyar wants to merge 1 commit into
Conversation
e59a8f7 to
9bbc870
Compare
|
thank you for catching this! I'm gonna run a few more tests with this change, and then should be able to merge this in. One thing before merging, commits must have verified signatures (if you set up a GPG key and set signing as default true, this will be resolved): https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification |
There was a problem hiding this comment.
this is a great catch and the fix works without increasing the parser.c size by much. I know that some edge cases are python specific, but in general this bug could also happen in tsql, javascript and maybe? ispl, for example with something like SELECT '}' FROM Foo:

I like that Python is parsed separately because it has edge cases that doesn't apply to the other languages. But, this made me realize that for the other languages we should probably also add this fix (something like if the delimiter is within a string it shouldn't be counted). I am going to work off of this PR to add this fix to tsql, ispl and javascript cases (this will be done in scanner.h and referenced from the udl and objectscript scanners.
`lex_fenced_text` balances braces without knowing anything about string
literals or comments, so a `{` inside a Python string ends the method body
early. A single line like
s = "dict literal { unbalanced"
leaves the counter unbalanced, and the body token swallows the rest of the
class. Every following member is lost from the tree, and the only signal is
an ERROR range at the closing brace of the class.
`external_method_body_content` is shared with XData (XML and JSON),
triggers, SQL queries and storage, and the scanner gets no language context
(`in_body` is never read). Teaching `lex_fenced_text` about quotes would
break XML prose containing apostrophes and SQL `--` comments, so this
splits the token instead of patching it in place:
- `_python_method_keywords` and `_python_trigger_keywords` match
`Language = python` only, so the grammar can select a different body
token. Both alias `method_keyword_python_language` back to
`method_keyword_external_language`.
- `python_method_body_content` is lexed by `lex_python_fenced_text`, which
tracks brace depth while skipping `#` comments, single and triple quotes,
prefixed strings, and backslash escapes. An unterminated single-quoted
string stops at the newline so brace counting resumes.
- `method_keyword_external_language` is narrowed to tsql and ispl. Those
two, plus XData, triggers, queries and storage, keep
`external_method_body_content` and the existing brace counter.
- The two Python rules in `injections.scm` point at the new token so Python
highlighting keeps working.
The only change to parser output is `external_method_body_content` ->
`python_method_body_content` on `Language = python` bodies.
The playground grammar keeps its own near-copy of the UDL scanner, so the
same changes are ported there; without that its external token indices skew
and unrelated Owner-keyword tests fail.
New corpus cases in `udl/test/corpus/python-method.txt` cover the brace in a
string, the quoting edge cases (triple quotes, f-strings, raw strings,
escaped quotes, comments, nested dict literals), a Python trigger, and a
tsql method as a control. All five grammars pass: 2745 parses, 0 failures.
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
9bbc870 to
f4c7c62
Compare
|
closing this, as it can't be merged with an unsigned commit, so this commit is going to merged with my commit in #52 |
|
oh, I amended the commit, after adding ssh signing key - hopefully that all actually worked! I will make sure future commits are signed :) |
Thank you! I merged your fix in with my commit as well with PR #52 |
A
{inside a Python string literal ends aLanguage = pythonmethod bodyearly, and everything after it in the class is lost from the tree.
Reproduction
Before this change,
tree-sitter parsegives oneexternal_method_body_contentspanning[4, 1] - [15, 0]: the body tokenruns past its own closing brace and consumes
Realalong with it.Realnever appears in the tree. The only signal is an ERROR range on the closing
brace of the class, so a consumer that tolerates partial parses sees a class
with one method instead of two.
The cause is
lex_fenced_textinudl/src/scanner.c. It counts{and}with no knowledge of string literals or comments, so the
{inside thestring increments the depth and the real closing brace only brings it back to
one.
Why this splits the token instead of patching the brace counter
external_method_body_contentis shared by XData (XML and JSON), triggers,SQL queries and storage. The scanner has no language context when it runs
(
in_bodyis declared and assigned but never read), so it cannot tell whichof those it is lexing.
Teaching
lex_fenced_textto skip quotes would therefore apply Python rulesto XML prose, where an apostrophe in
don'twould open a string that nevercloses, and to SQL, where
--is a comment and#is not. So the grammar nowselects a different body token when the language is Python:
_python_method_keywordsand_python_trigger_keywordsmatchLanguage = pythononly. Both aliasmethod_keyword_python_languagebackto
method_keyword_external_language, so the keyword node is unchanged.python_method_body_contentis lexed by a newlex_python_fenced_text,which tracks brace depth while skipping
#line comments, single and triplequotes, prefixed strings, and backslash escapes. An unterminated
single-quoted string stops at the newline so brace counting resumes on the
next line.
method_keyword_external_languageis narrowed totsqlandispl. Thosetwo, plus XData, triggers, queries and storage, keep
external_method_body_contentand the existing brace counter.injections.scmpoint at the new token, so Pythonhighlighting still works. Verified with
tree-sitter query: the injectionfires on the Python body and the range now stops at the correct brace.
Effect on parser output
One node name changes:
external_method_body_contentbecomespython_method_body_contentonLanguage = pythonbodies. The new keywordrules are hidden and aliased specifically to keep it to that. This is a breaking
change for a consumer matching that node on Python bodies, which is why it is
called out here rather than buried in the diff.
Playground grammar
objectscript/src/scanner.cis a near-copy of the UDL scanner, differing onlyin the function prefix and two core-scanner flags, so the same changes are
ported there. Without that, its external token indices skew against the
regenerated
grammar.jsonand unrelatedOwnerkeyword tests fail.Tests
New corpus cases in
udl/test/corpus/python-method.txt:still parse, including its nested
##class(...)call"close } brace",'{ open only',f"fstring {a} interp","""triple with } and { braces""",'''other triple }''',# comment with } and {,"escaped \" quote }",r"raw \ backslash }", and a real nested dict literal{"k": "v"}Language = tsqlmethod as a control, asserting it still producesexternal_method_body_contentThe existing
Valid ClassMethodcase inudl/test/corpus/class-method.txtis updated for the new body token name, and the mirrored playground corpus is
regenerated through the
pre-commitsync.All five grammars pass, 2745 parses and 0 failures:
objectscriptudlcoreexprobjectscript_routineAlso run locally:
npm run lint(clean),make checkquery,make lintquery,make formatquery(no diff),npm test(3/3, which compiles the modifiedscanner with no warnings),
cargo test --lib(4/4, including the injectionsquery load),
make test, and the CI examples loop overexamples/*.cls.Generated artifacts (
parser.c,grammar.json,node-types.json) arecommitted in sync with the grammar change per CONTRIBUTING, for the two
grammars that change:
udlandobjectscript.core,exprandobjectscript_routineare left untouched.udl/src/parser.cis large, so thediff is mostly that file.
Context
Found while indexing IRIS source with a tree-sitter based code graph tool,
where a single Python method with a brace in a string silently dropped the
rest of the class from the index.