diff --git a/CHANGELOG.md b/CHANGELOG.md index de2cc0bb0..3409b833d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **API:** `TypeParameterNode::getTypeParameters` method. + +### Fixed + +- Name resolution failures on generic routine invocations where later type parameters are constrained by earlier type parameters. +- Type resolution failures on `as` casts where the type is returned from a routine invocation. +- Inaccurate type resolution when calling a constructor on a class reference type. + ## [1.16.0] - 2025-05-09 ### Added diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/GenericDefinitionNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/GenericDefinitionNodeImpl.java index 7938ab814..c8b08ac53 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/GenericDefinitionNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/GenericDefinitionNodeImpl.java @@ -19,18 +19,12 @@ package au.com.integradev.delphi.antlr.ast.node; import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; -import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; -import com.google.common.collect.ImmutableList; import java.util.List; import java.util.stream.Collectors; import org.antlr.runtime.Token; -import org.sonar.plugins.communitydelphi.api.ast.ConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; import org.sonar.plugins.communitydelphi.api.ast.GenericDefinitionNode; -import org.sonar.plugins.communitydelphi.api.ast.NameDeclarationNode; import org.sonar.plugins.communitydelphi.api.ast.TypeParameterNode; -import org.sonar.plugins.communitydelphi.api.type.Constraint; -import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; public final class GenericDefinitionNodeImpl extends DelphiNodeImpl implements GenericDefinitionNode { @@ -53,22 +47,10 @@ public T accept(DelphiParserVisitor visitor, T data) { @Override public List getTypeParameters() { if (typeParameters == null) { - ImmutableList.Builder builder = ImmutableList.builder(); - - for (TypeParameterNode parameterNode : getTypeParameterNodes()) { - List constraints = - parameterNode.getConstraintNodes().stream() - .map(ConstraintNode::getConstraint) - .collect(Collectors.toUnmodifiableList()); - - for (NameDeclarationNode name : parameterNode.getTypeParameterNameNodes()) { - TypeParameterType type = TypeParameterTypeImpl.create(name.getImage(), constraints); - TypeParameter typeParameter = new TypeParameterImpl(name, type); - builder.add(typeParameter); - } - } - - typeParameters = builder.build(); + typeParameters = + getTypeParameterNodes().stream() + .flatMap(node -> node.getTypeParameters().stream()) + .collect(Collectors.toList()); } return typeParameters; @@ -92,24 +74,4 @@ public String getImage() { } return image; } - - private static final class TypeParameterImpl implements TypeParameter { - private final NameDeclarationNode location; - private final TypeParameterType type; - - private TypeParameterImpl(NameDeclarationNode location, TypeParameterType type) { - this.location = location; - this.type = type; - } - - @Override - public NameDeclarationNode getLocation() { - return location; - } - - @Override - public TypeParameterType getType() { - return type; - } - } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeParameterNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeParameterNodeImpl.java index 1a673781b..2fe6cc06e 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeParameterNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeParameterNodeImpl.java @@ -19,14 +19,18 @@ package au.com.integradev.delphi.antlr.ast.node; import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; import java.util.List; import java.util.stream.Collectors; import org.antlr.runtime.Token; import org.sonar.plugins.communitydelphi.api.ast.ConstraintNode; +import org.sonar.plugins.communitydelphi.api.ast.GenericDefinitionNode.TypeParameter; import org.sonar.plugins.communitydelphi.api.ast.NameDeclarationNode; import org.sonar.plugins.communitydelphi.api.ast.TypeConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.TypeParameterNode; import org.sonar.plugins.communitydelphi.api.ast.TypeReferenceNode; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; public final class TypeParameterNodeImpl extends DelphiNodeImpl implements TypeParameterNode { public TypeParameterNodeImpl(Token token) { @@ -61,4 +65,39 @@ public List getTypeConstraintNodes() { public List getConstraintNodes() { return findChildrenOfType(ConstraintNode.class); } + + @Override + public List getTypeParameters() { + List constraints = + getConstraintNodes().stream() + .map(ConstraintNode::getConstraint) + .collect(Collectors.toUnmodifiableList()); + + return getTypeParameterNameNodes().stream() + .map( + name -> + new TypeParameterImpl( + name, TypeParameterTypeImpl.create(name.getImage(), constraints))) + .collect(Collectors.toList()); + } + + private static final class TypeParameterImpl implements TypeParameter { + private final NameDeclarationNode location; + private final TypeParameterType type; + + public TypeParameterImpl(NameDeclarationNode location, TypeParameterType type) { + this.location = location; + this.type = type; + } + + @Override + public NameDeclarationNode getLocation() { + return location; + } + + @Override + public TypeParameterType getType() { + return type; + } + } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/SymbolTableVisitor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/SymbolTableVisitor.java index 8ffcf535b..74a4dcbbf 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/SymbolTableVisitor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/visitors/SymbolTableVisitor.java @@ -88,7 +88,6 @@ import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode; import org.sonar.plugins.communitydelphi.api.ast.FormalParameterNode.FormalParameterData; import org.sonar.plugins.communitydelphi.api.ast.GenericDefinitionNode; -import org.sonar.plugins.communitydelphi.api.ast.GenericDefinitionNode.TypeParameter; import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode; import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode; import org.sonar.plugins.communitydelphi.api.ast.ImplementationSectionNode; @@ -402,12 +401,14 @@ private static void createTypeParameterDeclarations( .map(TypeConstraintNodeImpl.class::cast) .map(TypeConstraintNodeImpl::getTypeNode) .forEach(data.nameResolutionHelper::resolve); - } - for (TypeParameter typeParameter : definition.getTypeParameters()) { - NameDeclarationNode location = typeParameter.getLocation(); - var declaration = new TypeParameterNameDeclarationImpl(location, typeParameter.getType()); - data.addDeclaration(declaration, location); + parameterNode + .getTypeParameters() + .forEach( + param -> + data.addDeclaration( + new TypeParameterNameDeclarationImpl(param.getLocation(), param.getType()), + param.getLocation())); } } } 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 a287c3505..717541c28 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 @@ -121,9 +121,14 @@ public Type resolve(PrimaryExpressionNode expression) { } private static Type classReferenceValueType(Type type) { + if (type.isProcedural()) { + type = ((ProceduralType) type).returnType(); + } + if (type.isClassReference()) { return ((ClassReferenceType) type).classType(); } + return unknownType(); } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolver.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolver.java index 15ac7a180..87487698c 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolver.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolver.java @@ -1103,17 +1103,27 @@ private void disambiguateArguments(List argumentExpressions, boo } private void resolveReturnType(Invocable invocable, List arguments) { - if (!isConstructor((NameDeclaration) invocable)) { - Type returnType = invocable.getReturnType(); - if (returnType instanceof IntrinsicReturnType) { - List argumentTypes = - arguments.stream() - .map(InvocationArgument::getType) - .collect(Collectors.toUnmodifiableList()); - returnType = ((IntrinsicReturnType) returnType).getReturnType(argumentTypes); + // Constructors are a special case - they return the type they are invoked on + if (isConstructor((NameDeclaration) invocable)) { + if (currentType.isClassReference()) { + // Calling the constructor on a class reference type returns an instance of that class + updateType(((ClassReferenceType) currentType).classType()); } - updateType(returnType); + + return; } + + Type returnType = invocable.getReturnType(); + + if (returnType instanceof IntrinsicReturnType) { + List argumentTypes = + arguments.stream() + .map(InvocationArgument::getType) + .collect(Collectors.toUnmodifiableList()); + returnType = ((IntrinsicReturnType) returnType).getReturnType(argumentTypes); + } + + updateType(returnType); } private void createCandidates(InvocationResolver resolver) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeSpecializationContextImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeSpecializationContextImpl.java index 7073921b0..1d4df592c 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeSpecializationContextImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeSpecializationContextImpl.java @@ -18,6 +18,7 @@ */ package au.com.integradev.delphi.type.generic; +import au.com.integradev.delphi.type.generic.constraint.TypeConstraintImpl; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -28,6 +29,8 @@ import org.sonar.plugins.communitydelphi.api.symbol.declaration.GenerifiableDeclaration; import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypedDeclaration; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.TypeConstraint; import org.sonar.plugins.communitydelphi.api.type.Type; import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; import org.sonar.plugins.communitydelphi.api.type.TypeSpecializationContext; @@ -66,14 +69,26 @@ public TypeSpecializationContextImpl(NameDeclaration declaration, List typ } } - private static boolean constraintViolated(Type parameter, Type argument) { + private boolean constraintViolated(Type parameter, Type argument) { return parameter.isTypeParameter() && ((TypeParameterType) parameter) .constraintItems().stream() + .map(this::specializeConstraint) .map(constraint -> constraint.satisfiedBy(argument)) .anyMatch(s -> !s); } + private Constraint specializeConstraint(Constraint constraint) { + if (constraint instanceof TypeConstraint) { + Type specializedType = getArgument(((TypeConstraint) constraint).type()); + if (specializedType != null) { + return new TypeConstraintImpl(specializedType); + } + } + + return constraint; + } + @Override @Nullable public Type getArgument(Type parameter) { diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeParameterNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeParameterNode.java index b98383a32..5a8af60dc 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeParameterNode.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeParameterNode.java @@ -19,6 +19,7 @@ package org.sonar.plugins.communitydelphi.api.ast; import java.util.List; +import org.sonar.plugins.communitydelphi.api.ast.GenericDefinitionNode.TypeParameter; public interface TypeParameterNode extends DelphiNode { List getTypeParameterNameNodes(); @@ -30,4 +31,6 @@ public interface TypeParameterNode extends DelphiNode { List getTypeConstraintNodes(); List getConstraintNodes(); + + List getTypeParameters(); } diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java index 888a51ed2..65eba3dca 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java @@ -458,7 +458,7 @@ void testClassReferenceArgumentResolution() { @Test void testClassReferenceConstructorTypeResolution() { execute("classReferences/ConstructorTypeResolution.pas"); - verifyUsages(15, 10, reference(22, 2)); + verifyUsages(15, 10, reference(22, 2), reference(23, 2)); verifyUsages(8, 16, reference(22, 11)); } @@ -517,7 +517,7 @@ void testCharTypeResolution() { @Test void testCastTypeResolution() { execute("typeResolution/Casts.pas"); - verifyUsages(8, 14, reference(15, 12), reference(16, 16)); + verifyUsages(8, 14, reference(22, 12), reference(23, 16), reference(24, 20)); } @Test @@ -1100,7 +1100,8 @@ void testGenericImplicitSpecializations() { @Test void testGenericConstraints() { execute("generics/Constraint.pas"); - verifyUsages(11, 14, reference(20, 25), reference(53, 8)); + verifyUsages(11, 14, reference(20, 25), reference(58, 8)); + verifyUsages(31, 20, reference(68, 8)); } @Test diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/classReferences/ConstructorTypeResolution.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/classReferences/ConstructorTypeResolution.pas index e04150101..c8c6e38e2 100644 --- a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/classReferences/ConstructorTypeResolution.pas +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/classReferences/ConstructorTypeResolution.pas @@ -20,6 +20,7 @@ procedure Proc(Obj: TObject); procedure Test(Bar: Boolean; Baz: TMetaFoo); begin Proc(Baz.Create(Bar)); + Proc(TMetaFoo.Create(Bar)); end; end. \ No newline at end of file diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/Constraint.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/Constraint.pas index c7287b83a..461a8cb64 100644 --- a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/Constraint.pas +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/Constraint.pas @@ -26,6 +26,11 @@ TFoo = class procedure Test; end; + TMock = class(TObject) + public + class function Mock(out Raw: T): I; + end; + implementation function TTest.SerializableClone: ISerializable; @@ -60,4 +65,5 @@ procedure TFoo.Test; initialization Foo := TFoo.Create(Test); Foo.Test; + TMock.Mock(Test); end. \ No newline at end of file diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/typeResolution/Casts.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/typeResolution/Casts.pas index 2164ba369..69b8b80a8 100644 --- a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/typeResolution/Casts.pas +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/typeResolution/Casts.pas @@ -8,12 +8,20 @@ TFoo = class(TObject) procedure Bar; end; + TFooClass = class of TFoo; + implementation +function GetClass: TFooClass; +begin + Result := TFoo; +end; + procedure Test(Arg: TObject); begin TFoo(Arg).Bar; (Arg as TFoo).Bar; + (Arg as GetClass).Bar; end; end.