diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2db1117..5601ef4a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for implicit `Self` in `Initialize` and `Finalize` operators, introduced in Delphi 13. - Support for `DCCARM64EC` toolchain, introduced in Delphi 13.1. - `NoreturnContract` analysis rule, which flags `noreturn` routines that return normally. +- Support for `if` expressions (e.g. `X := if Foo then Bar else Baz;`), introduced in Delphi 13. ## [1.18.3] - 2025-11-11 diff --git a/delphi-frontend/src/main/antlr3/au/com/integradev/delphi/antlr/Delphi.g b/delphi-frontend/src/main/antlr3/au/com/integradev/delphi/antlr/Delphi.g index 54ff76185..c39ba3133 100644 --- a/delphi-frontend/src/main/antlr3/au/com/integradev/delphi/antlr/Delphi.g +++ b/delphi-frontend/src/main/antlr3/au/com/integradev/delphi/antlr/Delphi.g @@ -72,6 +72,7 @@ tokens { TkArgument; TkAnonymousMethod; TkAnonymousMethodHeading; + TkIfExpression; TkLessThanEqual; TkGreaterThanEqual; } @@ -920,6 +921,10 @@ attribute : (ASSEMBLY ':')? expression (':' expression)* //---------------------------------------------------------------------------- expression : relationalExpression | anonymousMethod + | ifExpression + ; +ifExpression : IF expression THEN expression ELSE expression + -> ^(TkIfExpression IF expression THEN expression ELSE expression) ; // ANTLR sets the begin and end tokens for nested binary expression nodes // in relationalOperator, not relationalExpression, meaning that their diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/IfExpressionNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/IfExpressionNodeImpl.java new file mode 100644 index 000000000..601eb93ec --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/IfExpressionNodeImpl.java @@ -0,0 +1,79 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2026 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.antlr.ast.node; + +import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.symbol.resolve.ExpressionTypeResolver; +import javax.annotation.Nonnull; +import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; +import org.sonar.plugins.communitydelphi.api.type.Type; + +public final class IfExpressionNodeImpl extends ExpressionNodeImpl implements IfExpressionNode { + private String image; + + public IfExpressionNodeImpl(Token token) { + super(token); + } + + public IfExpressionNodeImpl(int tokenType) { + super(tokenType); + } + + @Override + public T accept(DelphiParserVisitor visitor, T data) { + return visitor.visit(this, data); + } + + @Override + public ExpressionNode getGuardExpression() { + return (ExpressionNode) getChild(1); + } + + @Override + public ExpressionNode getThenExpression() { + return (ExpressionNode) getChild(3); + } + + @Override + public ExpressionNode getElseExpression() { + return (ExpressionNode) getChild(5); + } + + @Override + public String getImage() { + if (image == null) { + image = + "if " + + getGuardExpression().getImage() + + " then " + + getThenExpression().getImage() + + " else " + + getElseExpression().getImage(); + } + return image; + } + + @Override + @Nonnull + protected Type createType() { + return new ExpressionTypeResolver(getTypeFactory()).resolve(this); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CognitiveComplexityVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CognitiveComplexityVisitor.java index 32ccaade4..1c4e4e574 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CognitiveComplexityVisitor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CognitiveComplexityVisitor.java @@ -31,6 +31,7 @@ import org.sonar.plugins.communitydelphi.api.ast.ExceptBlockNode; import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; import org.sonar.plugins.communitydelphi.api.ast.StatementNode; @@ -90,6 +91,19 @@ public Data visit(IfStatementNode statement, Data data) { return data; } + @Override + public Data visit(IfExpressionNode expression, Data data) { + data.increaseComplexityByNesting(); + expression.getGuardExpression().accept(this, data); + + ++data.nesting; + expression.getThenExpression().accept(this, data); + expression.getElseExpression().accept(this, data); + --data.nesting; + + return data; + } + @Override public Data visit(ExceptBlockNode exceptBlock, Data data) { if (exceptBlock.hasHandlers()) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CyclomaticComplexityVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CyclomaticComplexityVisitor.java index e4d832883..93bd73f84 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CyclomaticComplexityVisitor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/CyclomaticComplexityVisitor.java @@ -23,6 +23,7 @@ import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode; import org.sonar.plugins.communitydelphi.api.ast.RoutineImplementationNode; @@ -80,6 +81,12 @@ public Data visit(IfStatementNode statement, Data data) { return DelphiParserVisitor.super.visit(statement, data); } + @Override + public Data visit(IfExpressionNode expression, Data data) { + ++data.complexity; + return DelphiParserVisitor.super.visit(expression, data); + } + @Override public Data visit(BinaryExpressionNode expression, Data data) { BinaryOperator operator = expression.getOperator(); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/DelphiParserVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/DelphiParserVisitor.java index 81ebb7af7..b88a5f078 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/DelphiParserVisitor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/DelphiParserVisitor.java @@ -81,6 +81,7 @@ import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; import org.sonar.plugins.communitydelphi.api.ast.HelperTypeNode; import org.sonar.plugins.communitydelphi.api.ast.IdentifierNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ImplementationSectionNode; import org.sonar.plugins.communitydelphi.api.ast.ImportClauseNode; @@ -622,6 +623,10 @@ default T visit(AnonymousMethodNode node, T data) { return visit((ExpressionNode) node, data); } + default T visit(IfExpressionNode node, T data) { + return visit((ExpressionNode) node, data); + } + default T visit(ArrayConstructorNode node, T data) { return visit((ExpressionNode) node, data); } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java index 99b3ed841..8cb8366a4 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/cfg/ControlFlowGraphVisitor.java @@ -50,6 +50,7 @@ import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode; import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; import org.sonar.plugins.communitydelphi.api.ast.IntegerLiteralNode; import org.sonar.plugins.communitydelphi.api.ast.LabelStatementNode; @@ -277,6 +278,22 @@ public ControlFlowGraphBuilder visit(IfStatementNode node, ControlFlowGraphBuild return buildCondition(builder, node.getGuardExpression(), thenBlock, elseBlock); } + @Override + public ControlFlowGraphBuilder visit(IfExpressionNode node, ControlFlowGraphBuilder builder) { + ProtoBlock after = builder.getCurrentBlock(); + + builder.addBlockBefore(after); + build(node.getElseExpression(), builder); + ProtoBlock elseBlock = builder.getCurrentBlock(); + + builder.addBlockBefore(after); + build(node.getThenExpression(), builder); + ProtoBlock thenBlock = builder.getCurrentBlock(); + + builder.addBlock(ProtoBlockFactory.branch(node, thenBlock, elseBlock)); + return buildCondition(builder, node.getGuardExpression(), thenBlock, elseBlock); + } + private ControlFlowGraphBuilder buildCondition( ControlFlowGraphBuilder builder, ExpressionNode node, diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolver.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolver.java index 8c12323e9..c67a7f249 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolver.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolver.java @@ -36,6 +36,7 @@ import org.sonar.plugins.communitydelphi.api.ast.CommonDelphiNode; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode; import org.sonar.plugins.communitydelphi.api.ast.Node; import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode; @@ -93,6 +94,58 @@ public Type resolve(UnaryExpressionNode expression) { } } + public Type resolve(IfExpressionNode expression) { + Type thenType = expression.getThenExpression().getType(); + Type elseType = expression.getElseExpression().getType(); + + if (thenType.isUnknown()) { + return elseType; + } + if (elseType.isUnknown()) { + return thenType; + } + + if (thenType.is(elseType)) { + return thenType; + } + + EqualityType thenAsCommon = TypeComparer.compare(elseType, thenType); + EqualityType elseAsCommon = TypeComparer.compare(thenType, elseType); + + if (thenAsCommon == EqualityType.INCOMPATIBLE_TYPES + && elseAsCommon == EqualityType.INCOMPATIBLE_TYPES) { + return findCommonAncestor(thenType, elseType); + } + + if (thenAsCommon == EqualityType.INCOMPATIBLE_TYPES) { + return elseType; + } + if (elseAsCommon == EqualityType.INCOMPATIBLE_TYPES) { + return thenType; + } + + return elseAsCommon.ordinal() >= thenAsCommon.ordinal() ? elseType : thenType; + } + + private static Type findCommonAncestor(Type left, Type right) { + if (!left.isStruct() || !right.isStruct()) { + return unknownType(); + } + + for (Type leftAncestor = left.parent(); + !leftAncestor.isUnknown(); + leftAncestor = leftAncestor.parent()) { + for (Type rightAncestor = right.parent(); + !rightAncestor.isUnknown(); + rightAncestor = rightAncestor.parent()) { + if (leftAncestor.is(rightAncestor)) { + return leftAncestor; + } + } + } + return unknownType(); + } + public Type resolve(PrimaryExpressionNode expression) { Type type = unknownType(); boolean regularArrayProperty = false; diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/IfExpressionNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/IfExpressionNode.java new file mode 100644 index 000000000..c1ba4ff80 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/IfExpressionNode.java @@ -0,0 +1,27 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2026 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.communitydelphi.api.ast; + +public interface IfExpressionNode extends ExpressionNode { + ExpressionNode getGuardExpression(); + + ExpressionNode getThenExpression(); + + ExpressionNode getElseExpression(); +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/GrammarTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/GrammarTest.java index 5d7977810..b1b4c50cd 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/GrammarTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/GrammarTest.java @@ -141,6 +141,11 @@ void testParseAnonymousMethods() { assertParsed("AnonymousMethods.pas"); } + @Test + void testParseConditionalExpressions() { + assertParsed("ConditionalExpressions.pas"); + } + @Test void testParseGenerics() { assertParsed("Generics.pas"); diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java index 7c6571d5b..b4194da48 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/cfg/ControlFlowGraphTest.java @@ -58,6 +58,7 @@ import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode; import org.sonar.plugins.communitydelphi.api.ast.CommonDelphiNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; import org.sonar.plugins.communitydelphi.api.ast.FinallyBlockNode; import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode; @@ -342,6 +343,20 @@ void testEmptyIfElse() { block(element(NameReferenceNode.class, "A")).succeedsTo(0))); } + @Test + void testIfExpression() { + test( + List.of("X: Integer"), + "X := if A then Foo else Bar;", + checker( + block(element(NameReferenceNode.class, "A")) + .branchesTo(3, 2) + .withTerminator(IfExpressionNode.class), + block(element(NameReferenceNode.class, "Foo")).succeedsTo(1), + block(element(NameReferenceNode.class, "Bar")).succeedsTo(1), + block(element(NameReferenceNode.class, "X")).succeedsTo(0))); + } + @Test void testLocalVarDeclaration() { test( diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolverTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolverTest.java new file mode 100644 index 000000000..5686a51c9 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/symbol/resolve/ExpressionTypeResolverTest.java @@ -0,0 +1,107 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2026 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.symbol.resolve; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.CLASS; +import static org.sonar.plugins.communitydelphi.api.type.TypeFactory.unknownType; + +import au.com.integradev.delphi.utils.types.TypeFactoryUtils; +import au.com.integradev.delphi.utils.types.TypeMocker; +import org.junit.jupiter.api.Test; +import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode; +import org.sonar.plugins.communitydelphi.api.ast.IfExpressionNode; +import org.sonar.plugins.communitydelphi.api.type.IntrinsicType; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.TypeFactory; + +class ExpressionTypeResolverTest { + private static final TypeFactory FACTORY = TypeFactoryUtils.defaultFactory(); + private static final ExpressionTypeResolver RESOLVER = new ExpressionTypeResolver(FACTORY); + + @Test + void testIfExpressionWithSameTypeOnBothBranchesReturnsThatType() { + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + assertThat(resolve(integer, integer).is(integer)).isTrue(); + } + + @Test + void testIfExpressionWithUnknownThenBranchReturnsElseType() { + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + assertThat(resolve(unknownType(), integer).is(integer)).isTrue(); + } + + @Test + void testIfExpressionWithUnknownElseBranchReturnsThenType() { + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + assertThat(resolve(integer, unknownType()).is(integer)).isTrue(); + } + + @Test + void testIfExpressionPromotesIntegerToReal() { + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + Type real = FACTORY.getIntrinsic(IntrinsicType.DOUBLE); + assertThat(resolve(integer, real).is(real)).isTrue(); + assertThat(resolve(real, integer).is(real)).isTrue(); + } + + @Test + void testIfExpressionWidensSmallerIntegerToLarger() { + Type byteType = FACTORY.getIntrinsic(IntrinsicType.BYTE); + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + assertThat(resolve(byteType, integer).is(integer)).isTrue(); + assertThat(resolve(integer, byteType).is(integer)).isTrue(); + } + + @Test + void testIfExpressionWithDescendantAndAncestorClassReturnsAncestor() { + Type base = TypeMocker.struct("TBase", CLASS); + Type derived = TypeMocker.struct("TDerived", CLASS, base); + assertThat(resolve(derived, base).is(base)).isTrue(); + assertThat(resolve(base, derived).is(base)).isTrue(); + } + + @Test + void testIfExpressionWithSiblingClassesReturnsCommonAncestor() { + Type base = TypeMocker.struct("TBase", CLASS); + Type left = TypeMocker.struct("TLeft", CLASS, base); + Type right = TypeMocker.struct("TRight", CLASS, base); + assertThat(resolve(left, right).is(base)).isTrue(); + } + + @Test + void testIfExpressionWithUnrelatedTypesReturnsUnknown() { + Type record = TypeMocker.struct("TFoo", CLASS); + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + assertThat(resolve(record, integer).isUnknown()).isTrue(); + } + + private static Type resolve(Type thenType, Type elseType) { + IfExpressionNode node = mock(IfExpressionNode.class); + ExpressionNode thenExpression = mock(ExpressionNode.class); + ExpressionNode elseExpression = mock(ExpressionNode.class); + when(thenExpression.getType()).thenReturn(thenType); + when(elseExpression.getType()).thenReturn(elseType); + when(node.getThenExpression()).thenReturn(thenExpression); + when(node.getElseExpression()).thenReturn(elseExpression); + return RESOLVER.resolve(node); + } +} diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/grammar/ConditionalExpressions.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/grammar/ConditionalExpressions.pas new file mode 100644 index 000000000..8d86f63ee --- /dev/null +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/grammar/ConditionalExpressions.pas @@ -0,0 +1,36 @@ +unit ConditionalExpressions; + +interface + +implementation + +function Describe(aValue : Integer) : string; +begin + Result := if aValue > 0 then 'positive' else 'non-positive'; +end; + +function PickInt(aFlag : Boolean; aLhs, aRhs : Integer) : Integer; +begin + Result := if aFlag then aLhs else aRhs; +end; + +procedure UseInArgument; +begin + Describe(if True then 1 else 2); +end; + +function Nested(aA, aB : Boolean) : Integer; +begin + Result := + if aA then + if aB then 1 else 2 + else + if aB then 3 else 4; +end; + +function Mixed(aFlag : Boolean) : Integer; +begin + Result := (if aFlag then 10 else 20) + 1; +end; + +end.