From 39a53bdbe90ce7be6e2ec0dcb779d23a0e3de856 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Wed, 19 Aug 2026 12:49:27 +0200 Subject: [PATCH] Give ntConstant an end position A constant declaration produced a node with no end position at all, so a consumer working in line ranges could not tell how far the declaration reached. That silently truncates any constant whose value spans lines: const Banner = 'first part ' + 'second part'; reported only the first line, and a tool slicing that range dropped the continuation. ntConstant was built with FStack.Push, so it was a plain TSyntaxNode with nowhere to record an end. Two changes are needed, because the node the caller finally sees is not the node that was parsed: ConstantDeclaration now pushes a compound node and records its end, the same way TypeDeclaration already does. This gives the intermediate ConstList an accurate extent. ConstSection rebuilds each constant from that ConstList, so it also has to push a compound node and inherit the end from it. The start still comes from the name, which is what a caller looking for the constant expects; only the end is new. TCompoundSyntaxNode.AssignEndPositionFrom is the counterpart to the existing AssignPositionFrom, for exactly this rebuild case. Single-line constants keep ending on their own line - the new test asserts both directions, since an end that ran on to the next declaration would be no more useful than one that stopped short. Verified against the existing suite: 42 passing, with the one pre-existing Serialization.BinaryRoundTrip failure unchanged (line_seq holds a pointer value that does not survive a round trip, unrelated to this change). Co-Authored-By: Claude Opus 5 (1M context) --- Source/DelphiAST.Classes.pas | 8 +++++++ Source/DelphiAST.pas | 9 ++++++-- Test/UnitTests/DelphiAST.Tests.pas | 34 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Source/DelphiAST.Classes.pas b/Source/DelphiAST.Classes.pas index 10b785b..8752255 100644 --- a/Source/DelphiAST.Classes.pas +++ b/Source/DelphiAST.Classes.pas @@ -104,6 +104,8 @@ TCompoundSyntaxNode = class(TSyntaxNode) FEndLine: Integer; public function Clone: TSyntaxNode; override; + //counterpart to AssignPositionFrom, for a node rebuilt from a parsed one + procedure AssignEndPositionFrom(const Node: TCompoundSyntaxNode); property EndCol: Integer read FEndCol write FEndCol; property EndLine: Integer read FEndLine write FEndLine; @@ -629,6 +631,12 @@ procedure TSyntaxNode.AssignPositionFrom(const Node: TSyntaxNode); { TCompoundSyntaxNode } +procedure TCompoundSyntaxNode.AssignEndPositionFrom(const Node: TCompoundSyntaxNode); +begin + FEndCol := Node.EndCol; + FEndLine := Node.EndLine; +end; + function TCompoundSyntaxNode.Clone: TSyntaxNode; begin Result := inherited; diff --git a/Source/DelphiAST.pas b/Source/DelphiAST.pas index 7a1dcb5..d25d70f 100644 --- a/Source/DelphiAST.pas +++ b/Source/DelphiAST.pas @@ -1195,9 +1195,11 @@ procedure TPasSyntaxTreeBuilder.CompoundStatement; procedure TPasSyntaxTreeBuilder.ConstantDeclaration; begin - FStack.Push(ntConstant); + //compound: a constant's value can span lines, so record where it ends. as TypeDeclaration. + FStack.PushCompoundSyntaxNode(ntConstant); try inherited; + SetCurrentCompoundNodesEndPosition; finally FStack.Pop; end; @@ -1329,9 +1331,12 @@ procedure TPasSyntaxTreeBuilder.ConstSection; if Constant.Typ <> ntName then Continue; - Temp := FStack.Push(ConstList.Typ); + //compound: start from the name, end from the ConstList that measured the value. + Temp := FStack.PushCompoundSyntaxNode(ConstList.Typ); try Temp.AssignPositionFrom(Constant); + if ConstList is TCompoundSyntaxNode then + TCompoundSyntaxNode(Temp).AssignEndPositionFrom(TCompoundSyntaxNode(ConstList)); FStack.AddChild(Constant.Clone); if Assigned(TypeInfo) then diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index 6d15948..96cf3c6 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -177,6 +177,39 @@ procedure TestSourcePositions; end; end; +procedure TestConstantEndPosition; +var + Root, ConstsNode, Node, Spanning, Single: TSyntaxNode; +begin + Root := ParseSource('unit Consts;' + sLineBreak + 'interface' + sLineBreak + 'const' + + sLineBreak + ' Spanning = ''first part '' +' + sLineBreak + ' ''second part'';' + + sLineBreak + ' Single = 42;' + sLineBreak + 'implementation' + sLineBreak + 'end.'); + try + ConstsNode := FindDescendant(Root, ntConstants); + AssertNotNil(ConstsNode, 'No const section was produced'); + Spanning := nil; + Single := nil; + for Node in ConstsNode.ChildNodes do + if Node.Typ = ntConstant then + if Node.Line = 4 then + Spanning := Node + else if Node.Line = 6 then + Single := Node; + + AssertNotNil(Spanning, 'No constant starting on line 4'); + AssertTrue(Spanning is TCompoundSyntaxNode, + 'Constant node must be compound so it can carry an end position'); + AssertEquals(5, TCompoundSyntaxNode(Spanning).EndLine, + 'A multi-line constant must reach the last line of its value'); + + AssertNotNil(Single, 'No constant starting on line 6'); + AssertEquals(6, TCompoundSyntaxNode(Single).EndLine, + 'A single-line constant must end on its own line, not run on to what follows'); + finally + Root.Free; + end; +end; + procedure TestInvalidSyntax; var Root: TSyntaxNode; @@ -229,6 +262,7 @@ procedure RunAllTests; RunTest('AST.GenericRecordAndProperty', TestGenericRecordAndProperty); RunTest('Writer.LiteralsUnicodeAndXmlEscaping', TestLiteralsAndUnicode); RunTest('AST.SourcePositions', TestSourcePositions); + RunTest('AST.ConstantEndPosition', TestConstantEndPosition); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); {$IFNDEF FPC} RunTest('Serialization.BinaryRoundTrip', TestBinarySerializationRoundTrip);