Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Support for the `is not` operator, introduced in Delphi 13.
- Support for the `not in` operator, introduced in Delphi 13.
- **API:** `Node::getNodeId` method.
- **API:** `OperatorNode` node type.
- **API:** `BinaryOperatorNode` node type.
- **API:** `UnaryOperatorNode` node type.
Expand All @@ -35,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Incorrect token image on `>=` and `<=` operators.
- Incorrect branch selection around `{$IF}` conditions that can't be evaluated at compile time.
- Obscure bug where name resolution could occur in the wrong lexical scope.

## [1.20.0] - 2026-08-12

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static DelphiAstImpl create(DelphiFile delphiFile, DelphiNode root) {
if (root != null) {
root.getChildren().forEach(ast::addChild);
}
ast.initializeNodeIds();
return ast;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public abstract class DelphiNodeImpl implements MutableDelphiNode {
protected DelphiNode parent;
private List<DelphiNode> children;
private int childIndex;
private int nodeId = -1;
private DelphiToken firstToken;
private DelphiToken lastToken;
private DelphiScope scope;
Expand Down Expand Up @@ -142,6 +143,11 @@ public int getTokenIndex() {
return getFirstToken().getIndex();
}

@Override
public final int getNodeId() {
return nodeId;
}

@Override
public DelphiToken getToken() {
return token;
Expand Down Expand Up @@ -194,6 +200,20 @@ private DelphiToken findLastToken() {
return result;
}

protected final void initializeNodeIds() {
assignNodeIds(0);
}

private int assignNodeIds(int nextNodeId) {
this.nodeId = nextNodeId++;

for (DelphiNode child : getChildren()) {
nextNodeId = ((DelphiNodeImpl) child).assignNodeIds(nextNodeId);
}

return nextNodeId;
}

@Override
public DelphiNode getFirstChildWithTokenType(DelphiTokenType tokenType) {
for (DelphiNode child : getChildren()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
import org.sonar.plugins.communitydelphi.api.token.DelphiTokenType;

public final class SymbolicNode implements Node {
private static final AtomicInteger IMAGINARY_TOKEN_INDEX = new AtomicInteger(Integer.MIN_VALUE);
private static final AtomicInteger IMAGINARY_NODE_ID = new AtomicInteger(Integer.MIN_VALUE);
private final DelphiTokenType tokenType;
private final int nodeId;
private final int tokenIndex;
private final String image;
private final int beginLine;
Expand All @@ -44,6 +45,7 @@ public SymbolicNode(DelphiNode node) {

public SymbolicNode(DelphiNode node, DelphiScope scope) {
this(
node.getNodeId(),
node.getTokenType(),
node.getTokenIndex(),
node.getImage(),
Expand All @@ -56,6 +58,7 @@ public SymbolicNode(DelphiNode node, DelphiScope scope) {
}

private SymbolicNode(
int nodeId,
DelphiTokenType tokenType,
int tokenIndex,
String image,
Expand All @@ -65,6 +68,7 @@ private SymbolicNode(
int endColumn,
DelphiScope scope,
boolean isIncludedNode) {
this.nodeId = nodeId;
this.tokenType = tokenType;
this.tokenIndex = tokenIndex;
this.image = image;
Expand All @@ -78,8 +82,9 @@ private SymbolicNode(

public static SymbolicNode imaginary(String image, DelphiScope scope) {
return new SymbolicNode(
IMAGINARY_NODE_ID.incrementAndGet(),
DelphiTokenType.INVALID,
IMAGINARY_TOKEN_INDEX.incrementAndGet(),
-1,
image,
0,
0,
Expand All @@ -91,6 +96,7 @@ public static SymbolicNode imaginary(String image, DelphiScope scope) {

public static SymbolicNode fromRange(String image, DelphiNode begin, DelphiNode end) {
return new SymbolicNode(
begin.getNodeId(),
begin.getTokenType(),
begin.getTokenIndex(),
image,
Expand All @@ -102,6 +108,11 @@ public static SymbolicNode fromRange(String image, DelphiNode begin, DelphiNode
begin.getFirstToken().isIncludedToken() || end.getFirstToken().isIncludedToken());
}

@Override
public int getNodeId() {
return nodeId;
}

@Override
public DelphiTokenType getTokenType() {
return tokenType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ private void handleTypeParameterReferences(List<TypeReferenceNode> typeReference
for (int i = 0; i < typeParameters.size(); ++i) {
TypedDeclaration declaration = typeParameters.get(i);
NameReferenceNode typeReference = typeReferences.get(i).getNameNode();
NameOccurrenceImpl occurrence = new NameOccurrenceImpl(typeReference);
NameOccurrenceImpl occurrence = new NameOccurrenceImpl(typeReference.getIdentifier());

occurrence.setNameDeclaration(declaration);
((NameReferenceNodeImpl) typeReference).setNameOccurrence(occurrence);
Expand All @@ -552,7 +552,7 @@ private void handleTypeParameterForwardReferences(
NameDeclaration declaration = new TypeParameterNameDeclarationImpl(typeReference, type);
((DelphiScopeImpl) routineScope).addDeclaration(declaration);

NameOccurrenceImpl occurrence = new NameOccurrenceImpl(typeReference);
NameOccurrenceImpl occurrence = new NameOccurrenceImpl(typeReference.getIdentifier());
occurrence.setNameDeclaration(declaration);
((NameReferenceNodeImpl) typeReference).setNameOccurrence(occurrence);

Expand Down Expand Up @@ -784,7 +784,7 @@ private boolean matchReferenceToUnitNameDeclaration(
SymbolicNode symbolicNode =
SymbolicNode.fromRange(
referenceImage.toString(),
node,
node.getIdentifier(),
references.get(declarationParts.size() - 1).getIdentifier());

NameOccurrenceImpl occurrence = new NameOccurrenceImpl(symbolicNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ protected void addImport(FileScope scope) {
* @param scope The scope we want to associate the node to
*/
public void registerScope(Node node, DelphiScope scope) {
registeredScopes.put(node.getTokenIndex(), scope);
registeredScopes.put(node.getNodeId(), scope);
}

/**
Expand All @@ -156,7 +156,7 @@ public void registerScope(Node node, DelphiScope scope) {
* @param declaration The declaration we are registering
*/
public void registerDeclaration(Node node, NameDeclaration declaration) {
registeredDeclarations.put(node.getTokenIndex(), declaration);
registeredDeclarations.put(node.getNodeId(), declaration);
}

/**
Expand All @@ -166,7 +166,7 @@ public void registerDeclaration(Node node, NameDeclaration declaration) {
* @param occurrence The occurrence we are registering
*/
public void registerOccurrence(Node node, NameOccurrence occurrence) {
registeredOccurrences.put(node.getTokenIndex(), occurrence);
registeredOccurrences.put(node.getNodeId(), occurrence);
}

/**
Expand All @@ -177,7 +177,7 @@ public void registerOccurrence(Node node, NameOccurrence occurrence) {
* @param occurrence The occurrence we are registering
*/
public void registerOccurrence(ForInStatementNode node, EnumeratorOccurrence occurrence) {
registeredEnumeratorOccurrences.put(node.getTokenIndex(), occurrence);
registeredEnumeratorOccurrences.put(node.getNodeId(), occurrence);
}

/**
Expand All @@ -186,7 +186,7 @@ public void registerOccurrence(ForInStatementNode node, EnumeratorOccurrence occ
* @param node The node which we want to attach symbol information to
*/
public void attach(MutableDelphiNode node) {
node.setScope(registeredScopes.get(node.getTokenIndex()));
node.setScope(registeredScopes.get(node.getNodeId()));
}

/**
Expand All @@ -196,7 +196,7 @@ public void attach(MutableDelphiNode node) {
*/
public void attach(NameDeclarationNode node) {
((NameDeclarationNodeImpl) node)
.setNameDeclaration(registeredDeclarations.get(node.getTokenIndex()));
.setNameDeclaration(registeredDeclarations.get(node.getNodeId()));
}

/**
Expand All @@ -205,7 +205,9 @@ public void attach(NameDeclarationNode node) {
* @param node The node which we want to attach symbol information to
*/
public void attach(RoutineNameNode node) {
var declaration = (RoutineNameDeclaration) registeredDeclarations.get(node.getTokenIndex());
NameDeclarationNode nameNode = node.getNameDeclarationNode();
int nodeId = nameNode == null ? node.getNodeId() : nameNode.getNodeId();
var declaration = (RoutineNameDeclaration) registeredDeclarations.get(nodeId);
((RoutineNameNodeImpl) node).setRoutineNameDeclaration(declaration);
}

Expand All @@ -232,7 +234,7 @@ public void unregisterOccurrences() {
*/
public void attach(NameReferenceNode node) {
((NameReferenceNodeImpl) node)
.setNameOccurrence(registeredOccurrences.get(node.getTokenIndex()));
.setNameOccurrence(registeredOccurrences.get(node.getIdentifier().getNodeId()));
}

/**
Expand All @@ -242,7 +244,7 @@ public void attach(NameReferenceNode node) {
*/
public void attach(ArrayAccessorNode node) {
((ArrayAccessorNodeImpl) node)
.setImplicitNameOccurrence(registeredOccurrences.get(node.getTokenIndex()));
.setImplicitNameOccurrence(registeredOccurrences.get(node.getNodeId()));
}

/**
Expand All @@ -252,7 +254,7 @@ public void attach(ArrayAccessorNode node) {
*/
public void attach(ForInStatementNode node) {
((ForInStatementNodeImpl) node)
.setEnumeratorOccurrence(registeredEnumeratorOccurrences.get(node.getTokenIndex()));
.setEnumeratorOccurrence(registeredEnumeratorOccurrences.get(node.getNodeId()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@
import org.sonar.plugins.communitydelphi.api.token.DelphiTokenType;

public interface Node {
/**
* Returns the node's unique id
*
* @return Node id
*/
int getNodeId();

DelphiTokenType getTokenType();

/**
* Returns the node's unique token index
* Returns the token index of this node's first concrete token
*
* @return Token index
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import au.com.integradev.delphi.file.DelphiFile;
import au.com.integradev.delphi.utils.DelphiUtils;
import au.com.integradev.delphi.utils.files.DelphiFileUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.sonar.plugins.communitydelphi.api.ast.DelphiAst;
import org.sonar.plugins.communitydelphi.api.ast.DelphiNode;
Expand All @@ -51,10 +54,42 @@ void testNodesAreExpectedType() {
checkTypes(ast);
}

@Test
void testNodeIdsAreUniqueAndStableAcrossEquivalentBuilds() {
DelphiAst reparsed =
DelphiFile.from(DelphiUtils.getResource(TEST_FILE), DelphiFileUtils.mockConfig()).getAst();

List<DelphiNode> originalNodes = flatten(ast);
List<DelphiNode> reparsedNodes = flatten(reparsed);
List<Integer> tokenIndices =
originalNodes.stream().map(DelphiNode::getTokenIndex).collect(Collectors.toList());

assertThat(originalNodes).hasSameSizeAs(reparsedNodes);
assertThat(tokenIndices.stream().distinct().count()).isLessThan(tokenIndices.size());
assertThat(originalNodes).extracting(DelphiNode::getNodeId).doesNotHaveDuplicates();
assertThat(originalNodes.stream().map(DelphiNode::getNodeId).collect(Collectors.toList()))
.containsExactlyElementsOf(
reparsedNodes.stream().map(DelphiNode::getNodeId).collect(Collectors.toList()));
}

private static void checkTypes(DelphiNode node) {
assertThat(node).isInstanceOf(DelphiNode.class);
for (DelphiNode child : node.getChildren()) {
checkTypes(child);
}
}

private static List<DelphiNode> flatten(DelphiNode node) {
List<DelphiNode> nodes = new ArrayList<>();
flatten(node, nodes);
return nodes;
}

private static void flatten(DelphiNode node, List<DelphiNode> nodes) {
nodes.add(node);

for (DelphiNode child : node.getChildren()) {
flatten(child, nodes);
}
}
}
Loading
Loading