diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a61717f..f0a5ac253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **API:** `EnumeratorOccurrence` type. -- **API:** `EnumeratorOccurrence::getGetEnumerator` method. -- **API:** `EnumeratorOccurrence::getMoveNext` method. -- **API:** `EnumeratorOccurrence::getCurrent` method. - **API:** `ForInStatementNode.getEnumeratorOccurrence` method. - **API:** `TypeOfTypeNode::getTypeReferenceNode` method. +- **API:** `NameReferenceNode::getFirstName` method. +- **API:** `DelphiTokenType.TYPE_CONSTRAINT` token type. +- **API:** `ConstraintNode` node type. +- **API:** `ClassConstraintNode` node type. +- **API:** `ConstructorConstraintNode` node type. +- **API:** `RecordConstraintNode` node type. +- **API:** `TypeConstraintNode` node type. +- **API:** `TypeParameterNode::getConstraintNodes` method. +- **API:** `TypeParameterType::constraintItems` method. +- **API:** `Constraint` type. +- **API:** `Constraint.ClassConstraint` type. +- **API:** `Constraint.ConstructorConstraint` type. +- **API:** `Constraint.RecordConstraint` type. +- **API:** `Constraint.TypeConstraint` type. ### Changed - Detect tab-indented multiline strings in `TabulationCharacter`. - Improve support for evaluating name references in compiler directive expressions. +- Improve overload resolution in cases involving generic type parameter constraints. ### Deprecated @@ -29,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `getEnumeratorOccurrence` instead. - **API:** `ForInStatementNode::getCurrentDeclaration` method, get the declaration through `getEnumeratorOccurrence` instead. +- **API:** `TypeParameterNode::getTypeConstraintNodes` method, use `getConstraintNodes` instead. +- **API:** `TypeParameterType::constraints` method, use `constraintItems` instead. ### Fixed @@ -37,7 +51,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - False positives on enumerator property `Current` in `UnusedProperty`. - False positives on enums that are never referenced by name (but have used values) in `UnusedType`. - Name resolution failures in legacy initialization sections referencing the implementation section. -- Name resolution failures whena accessing ancestors of enclosing types from nested type methods. +- Name resolution failures when accessing ancestors of enclosing types from nested type methods. +- Name resolution failures on invocations of methods with generic open array parameters. +- Name resolution failures around `Create` calls on types with `constructor` constraints. - Incorrect file position calculation for multiline string tokens. - Analysis errors around `type of` type declarations. 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 11947bd7c..40feca4bd 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 @@ -64,6 +64,7 @@ tokens { TkCompilerDirective; TkRealNumber; TkTypeParameter; + TkTypeConstraint; TkForLoopVar; TkArrayAccessorNode; TkArrayConstructor; @@ -780,10 +781,10 @@ typeParameter : nameDeclaration (',' nameDeclaration)* (':' gene nameDeclaration (nameDeclaration)* (genericConstraint genericConstraint*)? ) ; -genericConstraint : typeReference - | RECORD - | CLASS - | CONSTRUCTOR +genericConstraint : typeReference -> ^(TkTypeConstraint typeReference) + | RECORD^ + | CLASS^ + | CONSTRUCTOR^ ; genericArguments : '<' typeReferenceOrStringOrFile (',' typeReferenceOrStringOrFile)* '>' -> ^(TkGenericArguments '<' typeReferenceOrStringOrFile (',' typeReferenceOrStringOrFile)* '>') diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ClassConstraintNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ClassConstraintNodeImpl.java new file mode 100644 index 000000000..1db6471bf --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ClassConstraintNodeImpl.java @@ -0,0 +1,41 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint.ClassConstraintImpl; +import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.ClassConstraintNode; +import org.sonar.plugins.communitydelphi.api.type.Constraint; + +public final class ClassConstraintNodeImpl extends DelphiNodeImpl implements ClassConstraintNode { + public ClassConstraintNodeImpl(Token token) { + super(token); + } + + @Override + public T accept(DelphiParserVisitor visitor, T data) { + return visitor.visit(this, data); + } + + @Override + public Constraint getConstraint() { + return ClassConstraintImpl.instance(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ConstructorConstraintNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ConstructorConstraintNodeImpl.java new file mode 100644 index 000000000..259a1bdb0 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/ConstructorConstraintNodeImpl.java @@ -0,0 +1,42 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint.ConstructorConstraintImpl; +import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.ConstructorConstraintNode; +import org.sonar.plugins.communitydelphi.api.type.Constraint; + +public final class ConstructorConstraintNodeImpl extends DelphiNodeImpl + implements ConstructorConstraintNode { + public ConstructorConstraintNodeImpl(Token token) { + super(token); + } + + @Override + public T accept(DelphiParserVisitor visitor, T data) { + return visitor.visit(this, data); + } + + @Override + public Constraint getConstraint() { + return ConstructorConstraintImpl.instance(); + } +} 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 3aa4c691e..7938ab814 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 @@ -24,13 +24,13 @@ 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.Type; +import org.sonar.plugins.communitydelphi.api.type.Constraint; import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; -import org.sonar.plugins.communitydelphi.api.type.Typed; public final class GenericDefinitionNodeImpl extends DelphiNodeImpl implements GenericDefinitionNode { @@ -56,9 +56,9 @@ public List getTypeParameters() { ImmutableList.Builder builder = ImmutableList.builder(); for (TypeParameterNode parameterNode : getTypeParameterNodes()) { - List constraints = - parameterNode.getTypeConstraintNodes().stream() - .map(Typed::getType) + List constraints = + parameterNode.getConstraintNodes().stream() + .map(ConstraintNode::getConstraint) .collect(Collectors.toUnmodifiableList()); for (NameDeclarationNode name : parameterNode.getTypeParameterNameNodes()) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/NameReferenceNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/NameReferenceNodeImpl.java index 4f05d7f59..3c40bbdc4 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/NameReferenceNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/NameReferenceNodeImpl.java @@ -155,6 +155,15 @@ public Type getType() { } } + @Override + public NameReferenceNode getFirstName() { + NameReferenceNode result = this; + while (result.getParent() instanceof NameReferenceNode) { + result = (NameReferenceNode) result.getParent(); + } + return result; + } + @Override public NameReferenceNode getLastName() { List flatNames = flatten(); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RecordConstraintNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RecordConstraintNodeImpl.java new file mode 100644 index 000000000..427c7bafa --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/RecordConstraintNodeImpl.java @@ -0,0 +1,41 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint.RecordConstraintImpl; +import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.RecordConstraintNode; +import org.sonar.plugins.communitydelphi.api.type.Constraint; + +public final class RecordConstraintNodeImpl extends DelphiNodeImpl implements RecordConstraintNode { + public RecordConstraintNodeImpl(Token token) { + super(token); + } + + @Override + public T accept(DelphiParserVisitor visitor, T data) { + return visitor.visit(this, data); + } + + @Override + public Constraint getConstraint() { + return RecordConstraintImpl.instance(); + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeConstraintNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeConstraintNodeImpl.java new file mode 100644 index 000000000..09ee6aef8 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TypeConstraintNodeImpl.java @@ -0,0 +1,56 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint.TypeConstraintImpl; +import org.antlr.runtime.Token; +import org.sonar.plugins.communitydelphi.api.ast.TypeConstraintNode; +import org.sonar.plugins.communitydelphi.api.ast.TypeReferenceNode; +import org.sonar.plugins.communitydelphi.api.type.Constraint; + +public final class TypeConstraintNodeImpl extends DelphiNodeImpl implements TypeConstraintNode { + private Constraint constraint; + + public TypeConstraintNodeImpl(Token token) { + super(token); + } + + public TypeConstraintNodeImpl(int tokenType) { + super(tokenType); + } + + @Override + public T accept(DelphiParserVisitor visitor, T data) { + return visitor.visit(this, data); + } + + @Override + public TypeReferenceNode getTypeNode() { + return (TypeReferenceNode) getChild(0); + } + + @Override + public Constraint getConstraint() { + if (constraint == null) { + constraint = new TypeConstraintImpl(getTypeNode().getType()); + } + return constraint; + } +} 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 1c13dbfca..1a673781b 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 @@ -20,8 +20,11 @@ import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; 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.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; @@ -44,8 +47,18 @@ public List getTypeParameterNameNodes() { return findChildrenOfType(NameDeclarationNode.class); } + @SuppressWarnings("removal") @Override public List getTypeConstraintNodes() { - return findChildrenOfType(TypeReferenceNode.class); + return getConstraintNodes().stream() + .filter(TypeConstraintNode.class::isInstance) + .map(TypeConstraintNode.class::cast) + .map(TypeConstraintNode::getTypeNode) + .collect(Collectors.toList()); + } + + @Override + public List getConstraintNodes() { + return findChildrenOfType(ConstraintNode.class); } } 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 498ef19b5..1193ef680 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 @@ -40,6 +40,7 @@ import org.sonar.plugins.communitydelphi.api.ast.BinaryExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode; import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ClassConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.ClassHelperTypeNode; import org.sonar.plugins.communitydelphi.api.ast.ClassReferenceTypeNode; import org.sonar.plugins.communitydelphi.api.ast.ClassTypeNode; @@ -49,6 +50,8 @@ import org.sonar.plugins.communitydelphi.api.ast.ConstDeclarationNode; import org.sonar.plugins.communitydelphi.api.ast.ConstSectionNode; import org.sonar.plugins.communitydelphi.api.ast.ConstStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.ConstraintNode; +import org.sonar.plugins.communitydelphi.api.ast.ConstructorConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.ContainsClauseNode; import org.sonar.plugins.communitydelphi.api.ast.DelphiAst; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; @@ -117,6 +120,7 @@ import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode; import org.sonar.plugins.communitydelphi.api.ast.RangeExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.RealLiteralNode; +import org.sonar.plugins.communitydelphi.api.ast.RecordConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.RecordExpressionItemNode; import org.sonar.plugins.communitydelphi.api.ast.RecordExpressionNode; import org.sonar.plugins.communitydelphi.api.ast.RecordHelperTypeNode; @@ -144,6 +148,7 @@ import org.sonar.plugins.communitydelphi.api.ast.SubRangeTypeNode; import org.sonar.plugins.communitydelphi.api.ast.TextLiteralNode; import org.sonar.plugins.communitydelphi.api.ast.TryStatementNode; +import org.sonar.plugins.communitydelphi.api.ast.TypeConstraintNode; import org.sonar.plugins.communitydelphi.api.ast.TypeDeclarationNode; import org.sonar.plugins.communitydelphi.api.ast.TypeNode; import org.sonar.plugins.communitydelphi.api.ast.TypeOfTypeNode; @@ -745,4 +750,25 @@ default T visit(WhileStatementNode node, T data) { default T visit(WithStatementNode node, T data) { return visit((StatementNode) node, data); } + + /* Constraints */ + default T visit(ConstraintNode node, T data) { + return visit((DelphiNode) node, data); + } + + default T visit(ClassConstraintNode node, T data) { + return visit((ConstraintNode) node, data); + } + + default T visit(ConstructorConstraintNode node, T data) { + return visit((ConstraintNode) node, data); + } + + default T visit(RecordConstraintNode node, T data) { + return visit((ConstraintNode) node, data); + } + + default T visit(TypeConstraintNode node, T data) { + return visit((ConstraintNode) node, data); + } } 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 4fb7f8b3e..2fc9636a3 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 @@ -24,6 +24,7 @@ import au.com.integradev.delphi.antlr.ast.node.MutableDelphiNode; import au.com.integradev.delphi.antlr.ast.node.NameDeclarationNodeImpl; import au.com.integradev.delphi.antlr.ast.node.RoutineNameNodeImpl; +import au.com.integradev.delphi.antlr.ast.node.TypeConstraintNodeImpl; import au.com.integradev.delphi.antlr.ast.node.WithStatementNodeImpl; import au.com.integradev.delphi.antlr.ast.visitors.SymbolTableVisitor.Data; import au.com.integradev.delphi.preprocessor.CompilerSwitchRegistry; @@ -392,7 +393,11 @@ private static void createTypeParameterDeclarations( @Nullable GenericDefinitionNode definition, Data data) { if (definition != null) { for (TypeParameterNode parameterNode : definition.getTypeParameterNodes()) { - parameterNode.getTypeConstraintNodes().forEach(data.nameResolutionHelper::resolve); + parameterNode.getConstraintNodes().stream() + .filter(TypeConstraintNodeImpl.class::isInstance) + .map(TypeConstraintNodeImpl.class::cast) + .map(TypeConstraintNodeImpl::getTypeNode) + .forEach(data.nameResolutionHelper::resolve); } for (TypeParameter typeParameter : definition.getTypeParameters()) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/InvocationCandidate.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/InvocationCandidate.java index cd188845f..c9df07270 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/InvocationCandidate.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/InvocationCandidate.java @@ -32,6 +32,8 @@ import org.sonar.plugins.communitydelphi.api.type.IntrinsicType; import org.sonar.plugins.communitydelphi.api.type.Parameter; import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.ArrayConstructorType; +import org.sonar.plugins.communitydelphi.api.type.Type.CollectionType; /** * Stores information about an invocation candidate, used for overload resolution. Based directly @@ -197,11 +199,20 @@ public static InvocationCandidate implicitSpecialization( Map argumentsByTypeParameters = new HashMap<>(); for (int i = 0; i < argumentTypes.size(); ++i) { Type parameterType = routineDeclaration.getParameter(i).getType(); + Type argumentType = argumentTypes.get(i); + + if (parameterType.isOpenArray()) { + Type argumentElementType = getArrayElementType(argumentType); + if (argumentElementType != null) { + parameterType = ((CollectionType) parameterType).elementType(); + argumentType = argumentElementType; + } + } + if (!parameterType.isTypeParameter()) { continue; } - Type argumentType = argumentTypes.get(i); Type existingMapping = argumentsByTypeParameters.get(parameterType); if (existingMapping != null && !existingMapping.is(argumentType)) { @@ -230,6 +241,33 @@ public static InvocationCandidate implicitSpecialization( return new InvocationCandidate(specialized); } + private static Type getArrayElementType(Type type) { + if (type.isArray()) { + return ((CollectionType) type).elementType(); + } else if (type.isArrayConstructor()) { + return getHomogeneousElementType((ArrayConstructorType) type); + } else { + return null; + } + } + + private static Type getHomogeneousElementType(ArrayConstructorType type) { + List elementTypes = type.elementTypes(); + if (elementTypes.isEmpty()) { + return null; + } + + Type result = elementTypes.get(0); + for (Type elementType : elementTypes) { + if (!elementType.is(result)) { + result = null; + break; + } + } + + return result; + } + @Override public boolean equals(Object o) { if (this == o) { 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 11922e7b2..e88b9fcfa 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 @@ -91,6 +91,9 @@ import org.sonar.plugins.communitydelphi.api.symbol.scope.RoutineScope; import org.sonar.plugins.communitydelphi.api.symbol.scope.TypeScope; import org.sonar.plugins.communitydelphi.api.symbol.scope.UnknownScope; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.ConstructorConstraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.TypeConstraint; import org.sonar.plugins.communitydelphi.api.type.IntrinsicType; import org.sonar.plugins.communitydelphi.api.type.Parameter; import org.sonar.plugins.communitydelphi.api.type.Type; @@ -582,6 +585,10 @@ private void readNameReference(NameReferenceNode node, boolean inAttribute) { return; } + if (handleTypeParameterConstructorInvocation(reference)) { + continue; + } + NameOccurrence occurrence = createNameOccurrence(reference, inAttribute); resolveNameReferenceOccurrence(occurrence, reference.nextName() == null); @@ -596,6 +603,16 @@ private void readNameReference(NameReferenceNode node, boolean inAttribute) { } } + private boolean handleTypeParameterConstructorInvocation(NameReferenceNode reference) { + if (isTypeParameterConstructorInvocation(reference)) { + if (currentType.isClassReference()) { + updateType(((ClassReferenceType) currentType).classType()); + } + return true; + } + return false; + } + private void resolveNameReferenceOccurrence(NameOccurrence occurrence, boolean isLastName) { addName((NameOccurrenceImpl) occurrence); searchForDeclaration(occurrence); @@ -644,7 +661,6 @@ private boolean nameResolutionFailed() { private void specializeDeclarations(NameOccurrence occurrence) { declarations = declarations.stream() - .map(NameDeclaration.class::cast) .map( declaration -> { List typeArguments = occurrence.getTypeArguments(); @@ -657,13 +673,41 @@ private void specializeDeclarations(NameOccurrence occurrence) { private boolean isExplicitArrayConstructorInvocation(NameReferenceNode reference) { return Iterables.getLast(resolvedDeclarations, null) instanceof TypeNameDeclaration && isDynamicArrayReference(currentType) - && reference.getLastName().getImage().equalsIgnoreCase("Create"); + && reference.getIdentifier().getImage().equalsIgnoreCase("Create"); } private static boolean isDynamicArrayReference(Type type) { return type.isClassReference() && ((ClassReferenceType) type).classType().isDynamicArray(); } + private boolean isTypeParameterConstructorInvocation(NameReferenceNode reference) { + Type type = currentType; + if (type.isClassReference()) { + type = ((ClassReferenceType) type).classType(); + } + + if (type.isTypeParameter()) { + boolean emptyArgumentList = (reference.nextName() != null); + if (!emptyArgumentList) { + NameReferenceNode firstName = reference.getFirstName(); + DelphiNode next = firstName.getParent().getChild(firstName.getChildIndex() + 1); + emptyArgumentList = next instanceof ArgumentListNode && ((ArgumentListNode) next).isEmpty(); + } + return emptyArgumentList + && ((TypeParameterType) type) + .constraintItems().stream().anyMatch(ConstructorConstraint.class::isInstance) + && reference.getIdentifier().getImage().equalsIgnoreCase("Create"); + } + + return false; + } + + private boolean isTypeParameterConstructorInvocation(ArgumentListNode argumentList) { + Node previous = argumentList.getParent().getChild(argumentList.getChildIndex() - 1); + return previous instanceof NameReferenceNode + && isTypeParameterConstructorInvocation(((NameReferenceNode) previous).getLastName()); + } + private void readPossibleUnitNameReference(NameReferenceNode node, boolean inAttribute) { NameResolver unitNameResolver = new NameResolver(((DelphiNodeImpl) node).getTypeFactory()); if (unitNameResolver.readUnitNameReference(node, inAttribute)) { @@ -877,7 +921,8 @@ private void handleArgumentList(ArgumentListNode node) { } } - if (handleExplicitArrayConstructorInvocation(node)) { + if (handleExplicitArrayConstructorInvocation(node) + || isTypeParameterConstructorInvocation(node)) { return; } @@ -891,7 +936,7 @@ private void handleArgumentList(ArgumentListNode node) { private boolean handleExplicitArrayConstructorInvocation(ArgumentListNode node) { Node previous = node.getParent().getChild(node.getChildIndex() - 1); if (previous instanceof NameReferenceNode - && isExplicitArrayConstructorInvocation(((NameReferenceNode) previous))) { + && isExplicitArrayConstructorInvocation(((NameReferenceNode) previous).getLastName())) { updateType(((ClassReferenceType) currentType).classType()); node.getArgumentNodes().stream() .map(ArgumentNode::getExpression) @@ -1418,12 +1463,15 @@ void searchForDeclaration(NameOccurrence occurrence) { */ private void searchForDeclarationInConstraintTypes(NameOccurrence occurrence) { TypeParameterType type = (TypeParameterType) currentType; - for (Type constraint : type.constraints()) { - if (constraint instanceof ScopedType) { - updateType(constraint); - searchForDeclaration(occurrence); - if (!declarations.isEmpty()) { - break; + for (Constraint constraint : type.constraintItems()) { + if (constraint instanceof TypeConstraint) { + Type constraintType = ((TypeConstraint) constraint).type(); + if (constraintType instanceof ScopedType) { + updateType(constraintType); + searchForDeclaration(occurrence); + if (!declarations.isEmpty()) { + break; + } } } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeParameterTypeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeParameterTypeImpl.java index bda1a23e9..a5dd7d09d 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeParameterTypeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/TypeParameterTypeImpl.java @@ -23,21 +23,27 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.ClassConstraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.ConstructorConstraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.RecordConstraint; +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; public final class TypeParameterTypeImpl extends TypeImpl implements TypeParameterType { private final String image; - private List constraints; + private List constraintItems; - private TypeParameterTypeImpl(String image, List constraints) { + private TypeParameterTypeImpl(String image, List constraintItems) { this.image = image; - this.constraints = ImmutableList.copyOf(constraints); + this.constraintItems = ImmutableList.copyOf(constraintItems); } - public static TypeParameterType create(String image, List constraints) { - return new TypeParameterTypeImpl(image, constraints); + public static TypeParameterType create(String image, List constraintItems) { + return new TypeParameterTypeImpl(image, constraintItems); } public static TypeParameterType create(String image) { @@ -55,9 +61,19 @@ public int size() { return 0; } + @SuppressWarnings("removal") @Override public List constraints() { - return constraints; + return constraintItems.stream() + .filter(TypeConstraint.class::isInstance) + .map(TypeConstraint.class::cast) + .map(TypeConstraint::type) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List constraintItems() { + return constraintItems; } @Override @@ -82,6 +98,28 @@ public Type specialize(TypeSpecializationContext context) { * @param fullType Type representing the full type declaration */ public void setFullType(TypeParameterType fullType) { - this.constraints = fullType.constraints(); + this.constraintItems = fullType.constraintItems(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(image); + + for (int i = 0; i < constraintItems.size(); i++) { + builder.append((i == 0) ? ": " : ", "); + + Constraint constraint = constraintItems.get(i); + if (constraint instanceof TypeConstraint) { + builder.append(((TypeConstraint) constraint).type().getImage()); + } else if (constraint instanceof ClassConstraint) { + builder.append("class"); + } else if (constraint instanceof ConstructorConstraint) { + builder.append("constructor"); + } else if (constraint instanceof RecordConstraint) { + builder.append("record"); + } + } + + return builder.toString(); } } 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 99dc0bbdb..7073921b0 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 @@ -29,6 +29,7 @@ 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.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; import org.sonar.plugins.communitydelphi.api.type.TypeSpecializationContext; public final class TypeSpecializationContextImpl implements TypeSpecializationContext { @@ -55,10 +56,24 @@ public TypeSpecializationContextImpl(NameDeclaration declaration, List typ for (int i = 0; i < typeParameters.size(); ++i) { Type parameter = typeParameters.get(i); Type argument = typeArguments.get(i); + + if (constraintViolated(parameter, argument)) { + argumentsByParameter.clear(); + break; + } + argumentsByParameter.put(parameter, argument); } } + private static boolean constraintViolated(Type parameter, Type argument) { + return parameter.isTypeParameter() + && ((TypeParameterType) parameter) + .constraintItems().stream() + .map(constraint -> constraint.satisfiedBy(argument)) + .anyMatch(s -> !s); + } + @Override @Nullable public Type getArgument(Type parameter) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintImpl.java new file mode 100644 index 000000000..ee5551c2d --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintImpl.java @@ -0,0 +1,59 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import org.sonar.plugins.communitydelphi.api.type.Constraint.ClassConstraint; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.PointerType; + +public class ClassConstraintImpl extends ConstraintImpl implements ClassConstraint { + private static final ClassConstraintImpl INSTANCE = new ClassConstraintImpl(); + + private ClassConstraintImpl() { + // Hide constructor + } + + @Override + protected ConstraintCheckResult check(Type type) { + if (type.isClass() || (type.isPointer() && ((PointerType) type).isNilPointer())) { + return ConstraintCheckResult.SATISFIED; + } else { + return ConstraintCheckResult.VIOLATED; + } + } + + @Override + protected ConstraintCheckResult check(ClassConstraint constraint) { + return ConstraintCheckResult.SATISFIED; + } + + @Override + protected ConstraintCheckResult check(ConstructorConstraint constraint) { + return ConstraintCheckResult.COMPATIBLE; + } + + @Override + protected ConstraintCheckResult check(RecordConstraint constraint) { + return ConstraintCheckResult.VIOLATED; + } + + public static ClassConstraintImpl instance() { + return INSTANCE; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstraintImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstraintImpl.java new file mode 100644 index 000000000..530045660 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstraintImpl.java @@ -0,0 +1,83 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.TypeParameterType; + +abstract class ConstraintImpl implements Constraint { + protected enum ConstraintCheckResult { + VIOLATED, + COMPATIBLE, + SATISFIED + } + + @Override + public final boolean satisfiedBy(Type type) { + if (type instanceof TypeParameterType) { + ConstraintCheckResult result = ConstraintCheckResult.COMPATIBLE; + for (Constraint constraint : ((TypeParameterType) type).constraintItems()) { + switch (check(constraint)) { + case VIOLATED: + return false; + case COMPATIBLE: + break; + case SATISFIED: + result = ConstraintCheckResult.SATISFIED; + break; + } + } + return result == ConstraintCheckResult.SATISFIED; + } else { + return check(type) == ConstraintCheckResult.SATISFIED; + } + } + + private ConstraintCheckResult check(Constraint constraint) { + if (constraint instanceof TypeConstraint) { + return check(((TypeConstraint) constraint).type()); + } + + if (constraint instanceof ClassConstraint) { + return check((ClassConstraint) constraint); + } + + if (constraint instanceof ConstructorConstraint) { + return check((ConstructorConstraint) constraint); + } + + if (constraint instanceof RecordConstraint) { + return check((RecordConstraint) constraint); + } + + return ConstraintCheckResult.VIOLATED; + } + + protected abstract ConstraintCheckResult check(Type type); + + @SuppressWarnings("overloads") + protected abstract ConstraintCheckResult check(ClassConstraint constraint); + + @SuppressWarnings("overloads") + protected abstract ConstraintCheckResult check(ConstructorConstraint constraint); + + @SuppressWarnings("overloads") + protected abstract ConstraintCheckResult check(RecordConstraint constraint); +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintImpl.java new file mode 100644 index 000000000..11859f811 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintImpl.java @@ -0,0 +1,91 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import org.sonar.plugins.communitydelphi.api.ast.Visibility.VisibilityType; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineKind; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; +import org.sonar.plugins.communitydelphi.api.type.Constraint.ConstructorConstraint; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.PointerType; +import org.sonar.plugins.communitydelphi.api.type.Type.ScopedType; + +public class ConstructorConstraintImpl extends ConstraintImpl implements ConstructorConstraint { + private static final ConstructorConstraintImpl INSTANCE = new ConstructorConstraintImpl(); + + private ConstructorConstraintImpl() { + // Hide constructor + } + + @Override + protected ConstraintCheckResult check(Type type) { + if (type.isDynamicArray() + || type.isFixedArray() + || (type.isPointer() && ((PointerType) type).isNilPointer()) + || declaresPublicDefaultConstructor(type)) { + return ConstraintCheckResult.SATISFIED; + } else { + return ConstraintCheckResult.VIOLATED; + } + } + + @Override + protected ConstraintCheckResult check(ClassConstraint constraint) { + return ConstraintCheckResult.COMPATIBLE; + } + + @Override + protected ConstraintCheckResult check(ConstructorConstraint constraint) { + return ConstraintCheckResult.SATISFIED; + } + + @Override + protected ConstraintCheckResult check(RecordConstraint constraint) { + return ConstraintCheckResult.VIOLATED; + } + + private static boolean declaresPublicDefaultConstructor(Type type) { + while (type.isClass()) { + VisibilityType constructorVisibility = + ((ScopedType) type) + .typeScope().getRoutineDeclarations().stream() + .filter(ConstructorConstraintImpl::isDefaultConstructor) + .map(RoutineNameDeclaration::getVisibility) + .findFirst() + .orElse(null); + + if (constructorVisibility != null) { + return constructorVisibility == VisibilityType.PUBLIC; + } + + type = type.parent(); + } + return false; + } + + private static boolean isDefaultConstructor(RoutineNameDeclaration routine) { + return routine.getRoutineKind() == RoutineKind.CONSTRUCTOR + && routine.getParametersCount() == 0 + && routine.getName().equalsIgnoreCase("Create"); + } + + public static ConstructorConstraintImpl instance() { + return INSTANCE; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintImpl.java new file mode 100644 index 000000000..65670bddd --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintImpl.java @@ -0,0 +1,64 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import org.sonar.plugins.communitydelphi.api.type.Constraint.RecordConstraint; +import org.sonar.plugins.communitydelphi.api.type.Type; + +public class RecordConstraintImpl extends ConstraintImpl implements RecordConstraint { + private static final RecordConstraintImpl INSTANCE = new RecordConstraintImpl(); + + private RecordConstraintImpl() { + // Hide constructor + } + + @Override + protected ConstraintCheckResult check(Type type) { + if (type.isRecord() + || type.isBoolean() + || type.isReal() + || type.isInteger() + || type.isEnum() + || type.isSubrange() + || type.isChar()) { + return ConstraintCheckResult.SATISFIED; + } else { + return ConstraintCheckResult.VIOLATED; + } + } + + @Override + protected ConstraintCheckResult check(ClassConstraint constraint) { + return ConstraintCheckResult.VIOLATED; + } + + @Override + protected ConstraintCheckResult check(ConstructorConstraint constraint) { + return ConstraintCheckResult.VIOLATED; + } + + @Override + protected ConstraintCheckResult check(RecordConstraint constraint) { + return ConstraintCheckResult.SATISFIED; + } + + public static RecordConstraintImpl instance() { + return INSTANCE; + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintImpl.java new file mode 100644 index 000000000..376cb26d4 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintImpl.java @@ -0,0 +1,66 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.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.ClassReferenceType; + +public class TypeConstraintImpl extends ConstraintImpl implements TypeConstraint { + private final Type type; + + public TypeConstraintImpl(Type type) { + this.type = type; + } + + @Override + public Type type() { + return type; + } + + @Override + protected ConstraintCheckResult check(Type type) { + if (type.isClassReference()) { + // This is a compiler bug. + // See: https://embt.atlassian.net/servicedesk/customer/portal/1/RSS-3319 + type = ((ClassReferenceType) type).classType(); + } + + if (type.is(this.type) || type.isDescendantOf(this.type)) { + return ConstraintCheckResult.SATISFIED; + } else { + return ConstraintCheckResult.VIOLATED; + } + } + + @Override + protected ConstraintCheckResult check(ClassConstraint constraint) { + return ConstraintCheckResult.COMPATIBLE; + } + + @Override + protected ConstraintCheckResult check(ConstructorConstraint constraint) { + return ConstraintCheckResult.COMPATIBLE; + } + + @Override + protected ConstraintCheckResult check(RecordConstraint constraint) { + return ConstraintCheckResult.VIOLATED; + } +} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ClassConstraintNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ClassConstraintNode.java new file mode 100644 index 000000000..b18150c75 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ClassConstraintNode.java @@ -0,0 +1,21 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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 ClassConstraintNode extends ConstraintNode {} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstraintNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstraintNode.java new file mode 100644 index 000000000..472e2e130 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstraintNode.java @@ -0,0 +1,25 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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; + +import org.sonar.plugins.communitydelphi.api.type.Constraint; + +public interface ConstraintNode extends DelphiNode { + Constraint getConstraint(); +} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstructorConstraintNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstructorConstraintNode.java new file mode 100644 index 000000000..b128f12a0 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/ConstructorConstraintNode.java @@ -0,0 +1,21 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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 ConstructorConstraintNode extends ConstraintNode {} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/NameReferenceNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/NameReferenceNode.java index 32231b610..7426bb424 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/NameReferenceNode.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/NameReferenceNode.java @@ -39,6 +39,8 @@ public interface NameReferenceNode extends DelphiNode, Qualifiable, Typed { NameDeclaration getNameDeclaration(); + NameReferenceNode getFirstName(); + NameReferenceNode getLastName(); boolean isExplicitArrayConstructorInvocation(); diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RecordConstraintNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RecordConstraintNode.java new file mode 100644 index 000000000..b28e77c31 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/RecordConstraintNode.java @@ -0,0 +1,21 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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 RecordConstraintNode extends ConstraintNode {} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeConstraintNode.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeConstraintNode.java new file mode 100644 index 000000000..ca4450c70 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/ast/TypeConstraintNode.java @@ -0,0 +1,23 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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 TypeConstraintNode extends ConstraintNode { + TypeReferenceNode getTypeNode(); +} 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 e604420f4..b98383a32 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 @@ -23,5 +23,11 @@ public interface TypeParameterNode extends DelphiNode { List getTypeParameterNameNodes(); + /** + * @deprecated Use {@link TypeParameterNode#getConstraintNodes} instead. + */ + @Deprecated(forRemoval = true) List getTypeConstraintNodes(); + + List getConstraintNodes(); } diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Constraint.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Constraint.java new file mode 100644 index 000000000..bb58950d6 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Constraint.java @@ -0,0 +1,33 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type; + +public interface Constraint { + boolean satisfiedBy(Type type); + + interface ClassConstraint extends Constraint {} + + interface ConstructorConstraint extends Constraint {} + + interface RecordConstraint extends Constraint {} + + interface TypeConstraint extends Constraint { + Type type(); + } +} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Type.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Type.java index e8e5f8ff5..155497502 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Type.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/type/Type.java @@ -564,18 +564,26 @@ interface AliasType extends Type { } interface TypeParameterType extends Type { + /** + * The constraint types for this type parameter. + * + * @return list of constraint types + * @deprecated Use {@link TypeParameterType#constraintItems} instead. + */ + @Deprecated(forRemoval = true) + List constraints(); /** - * The set of constraint types for this type parameter. + * The constraints for this type parameter. * - *

For example, if we're constrained by a class type, then a generic specialization will - * require the type argument to be assignment-compatible with that class type. + *

For example, if we're constrained by TFoo, then generic specialization will require the + * type argument to be assignment-compatible with TFoo. * - * @return list of constraint types - * @see + * @return list of constraints + * @see * Constraints in Generics */ - List constraints(); + List constraintItems(); } interface IntegerType extends Type { 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 be2cce01e..193706998 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 @@ -1053,7 +1053,8 @@ void testGenericParameterizedMethods() { @Test void testGenericImplicitSpecializations() { execute("generics/ImplicitSpecialization.pas"); - verifyUsages(14, 14, reference(19, 20), reference(26, 2), reference(27, 2), reference(28, 2)); + verifyUsages(15, 14, reference(21, 20), reference(37, 2), reference(38, 2), reference(39, 2)); + verifyUsages(16, 14, reference(26, 20), reference(48, 2), reference(49, 2), reference(50, 2)); } @Test @@ -1084,7 +1085,7 @@ void testGenericTypeParameterNameDeclarations() { } @Test - void testGenericTypeParameterConflicts() { + void testGenericTypeParameterNameConflict() { execute("generics/TypeParameterNameConflict.pas"); verifyUsages(7, 14, reference(25, 8)); verifyUsages(11, 14, reference(26, 6), reference(27, 9)); @@ -1093,6 +1094,12 @@ void testGenericTypeParameterConflicts() { 16, 19, reference(16, 33), reference(21, 27), reference(21, 35), reference(23, 10)); } + @Test + void testGenericTypeParameterConstructor() { + execute("generics/TypeParameterConstructor.pas"); + verifyUsages(10, 19, reference(17, 20), reference(24, 21), reference(25, 23)); + } + @Test void testPropertySpecialization() { execute("generics/PropertySpecialization.pas"); diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/factory/TypeAliasGeneratorTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/factory/TypeAliasGeneratorTest.java index f931e4bc3..7c8a97068 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/factory/TypeAliasGeneratorTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/factory/TypeAliasGeneratorTest.java @@ -25,6 +25,7 @@ import au.com.integradev.delphi.type.UnresolvedTypeImpl; import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; +import au.com.integradev.delphi.type.generic.constraint.TypeConstraintImpl; import au.com.integradev.delphi.utils.types.TypeFactoryUtils; import au.com.integradev.delphi.utils.types.TypeMocker; import java.util.Collections; @@ -127,7 +128,8 @@ public Stream provideArguments(ExtensionContext context) { Arguments.of(FACTORY.subrange(ALIASED_NAME, BYTE), SubrangeType.class), Arguments.of(FACTORY.classOf(ALIASED_NAME, BYTE), ClassReferenceType.class), Arguments.of( - TypeParameterTypeImpl.create(ALIASED_NAME, List.of(BYTE)), TypeParameterType.class), + TypeParameterTypeImpl.create(ALIASED_NAME, List.of(new TypeConstraintImpl(BYTE))), + TypeParameterType.class), Arguments.of(BYTE, IntegerType.class), Arguments.of(FACTORY.getIntrinsic(IntrinsicType.DOUBLE), RealType.class), Arguments.of(FACTORY.getIntrinsic(IntrinsicType.BOOLEAN), BooleanType.class), @@ -214,6 +216,7 @@ void testUnhandledTypeInterfaceShouldThrow() { .isInstanceOf(AssertionError.class); } + @SuppressWarnings("removal") private static void assertAliasImplementsTypeThroughDelegation(Type alias, Type aliased) { assertDelegated(Type::parent, alias, aliased); assertDelegated(Type::ancestorList, alias, aliased); @@ -300,6 +303,7 @@ private static void assertAliasImplementsTypeThroughDelegation(Type alias, Type if (aliased instanceof TypeParameterType) { assertDelegated(TypeParameterType::constraints, alias, aliased); + assertDelegated(TypeParameterType::constraintItems, alias, aliased); } if (aliased instanceof IntegerType) { diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintTest.java new file mode 100644 index 000000000..32e5f3c37 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ClassConstraintTest.java @@ -0,0 +1,101 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.CLASS; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.RECORD; + +import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; +import au.com.integradev.delphi.utils.types.TypeFactoryUtils; +import au.com.integradev.delphi.utils.types.TypeMocker; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +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 ClassConstraintTest { + private static final TypeFactory FACTORY = TypeFactoryUtils.defaultFactory(); + + private static class SatisfiedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(TypeMocker.struct("TBar", CLASS)), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(ClassConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of(ClassConstraintImpl.instance(), ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + new TypeConstraintImpl(TypeMocker.struct("TBar", CLASS)), + ClassConstraintImpl.instance(), + ConstructorConstraintImpl.instance())))); + } + } + + private static class ViolatedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.BYTE)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.STRING)), + Arguments.of(TypeMocker.struct("TFoo", RECORD)), + Arguments.of(TypeParameterTypeImpl.create("T")), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(mock(Constraint.class)))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(RecordConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of(RecordConstraintImpl.instance(), ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + RecordConstraintImpl.instance(), + ConstructorConstraintImpl.instance(), + ClassConstraintImpl.instance())))); + } + } + + @ParameterizedTest + @ArgumentsSource(SatisfiedArgumentsProvider.class) + void testSatisfied(Type argumentType) { + assertThat(ClassConstraintImpl.instance().satisfiedBy(argumentType)).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(ViolatedArgumentsProvider.class) + void testViolated(Type argumentType) { + assertThat(ClassConstraintImpl.instance().satisfiedBy(argumentType)).isFalse(); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintTest.java new file mode 100644 index 000000000..cef70a088 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/ConstructorConstraintTest.java @@ -0,0 +1,177 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.CLASS; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.RECORD; + +import au.com.integradev.delphi.symbol.SymbolicNode; +import au.com.integradev.delphi.symbol.declaration.RoutineNameDeclarationImpl; +import au.com.integradev.delphi.symbol.scope.DelphiScopeImpl; +import au.com.integradev.delphi.type.factory.TypeFactoryImpl; +import au.com.integradev.delphi.type.factory.VoidTypeImpl; +import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; +import au.com.integradev.delphi.utils.types.TypeFactoryUtils; +import au.com.integradev.delphi.utils.types.TypeMocker; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.sonar.plugins.communitydelphi.api.ast.Visibility.VisibilityType; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineKind; +import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration; +import org.sonar.plugins.communitydelphi.api.symbol.scope.DelphiScope; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.IntrinsicType; +import org.sonar.plugins.communitydelphi.api.type.Parameter; +import org.sonar.plugins.communitydelphi.api.type.Type; +import org.sonar.plugins.communitydelphi.api.type.Type.ProceduralType.ProceduralKind; +import org.sonar.plugins.communitydelphi.api.type.Type.StructType; +import org.sonar.plugins.communitydelphi.api.type.TypeFactory; + +class ConstructorConstraintTest { + private static final TypeFactory FACTORY = TypeFactoryUtils.defaultFactory(); + + private static class SatisfiedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + var defaultConstructor = constructor("Create", VisibilityType.PUBLIC); + var lowercaseDefaultConstructor = constructor("create", VisibilityType.PUBLIC); + return Stream.of( + Arguments.of(addDeclaration(mockClass(), defaultConstructor)), + Arguments.of(addDeclaration(mockClass(), lowercaseDefaultConstructor)), + Arguments.of(mockClass(addDeclaration(mockClass(), defaultConstructor))), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of(ClassConstraintImpl.instance(), ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + new TypeConstraintImpl(addDeclaration(mockClass(), defaultConstructor)), + ClassConstraintImpl.instance(), + ConstructorConstraintImpl.instance())))); + } + } + + private static class ViolatedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + var defaultConstructor = constructor("Create", VisibilityType.PUBLIC); + var classWithDefaultConstructor = addDeclaration(mockClass(), defaultConstructor); + + var privateConstructor = constructor("Create", VisibilityType.PRIVATE); + var protectedConstructor = constructor("Create", VisibilityType.PROTECTED); + var publishedConstructor = constructor("Create", VisibilityType.PUBLISHED); + var wrongNameConstructor = constructor("NotCreate", VisibilityType.PUBLIC); + var parametersConstructor = constructor("Create", VisibilityType.PUBLIC, true); + + return Stream.of( + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.BYTE)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.STRING)), + Arguments.of(TypeMocker.struct("TFoo", RECORD)), + Arguments.of(mockClass()), + Arguments.of(addDeclaration(mockClass(), privateConstructor)), + Arguments.of(addDeclaration(mockClass(), protectedConstructor)), + Arguments.of(addDeclaration(mockClass(), publishedConstructor)), + Arguments.of(addDeclaration(mockClass(), wrongNameConstructor)), + Arguments.of(addDeclaration(mockClass(), parametersConstructor)), + Arguments.of(addDeclaration(mockClass(), parametersConstructor)), + Arguments.of(addDeclaration(mockClass(classWithDefaultConstructor), privateConstructor)), + Arguments.of(TypeParameterTypeImpl.create("T")), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(mock(Constraint.class)))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(RecordConstraintImpl.instance()))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(ClassConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of(RecordConstraintImpl.instance(), ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + RecordConstraintImpl.instance(), + ConstructorConstraintImpl.instance(), + ClassConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(new TypeConstraintImpl(mockClass()))))); + } + } + + private static StructType mockClass() { + return TypeMocker.struct("TFoo", CLASS); + } + + private static StructType mockClass(Type parent) { + return TypeMocker.struct("TFoo", CLASS, parent); + } + + private static StructType addDeclaration(StructType type, NameDeclaration declaration) { + ((DelphiScopeImpl) type.typeScope()).addDeclaration(declaration); + return type; + } + + private static RoutineNameDeclaration constructor(String name, VisibilityType visibility) { + return constructor(name, visibility, false); + } + + private static RoutineNameDeclaration constructor( + String name, VisibilityType visibility, boolean hasParameters) { + return new RoutineNameDeclarationImpl( + SymbolicNode.imaginary(name, DelphiScope.unknownScope()), + "Test.TFoo." + name, + VoidTypeImpl.instance(), + Collections.emptySet(), + false, + true, + RoutineKind.CONSTRUCTOR, + ((TypeFactoryImpl) FACTORY) + .createProcedural( + ProceduralKind.ROUTINE, + hasParameters ? List.of(mock(Parameter.class)) : Collections.emptyList(), + VoidTypeImpl.instance(), + Collections.emptySet()), + null, + visibility, + Collections.emptyList(), + Collections.emptyList()); + } + + @ParameterizedTest + @ArgumentsSource(SatisfiedArgumentsProvider.class) + void testSatisfied(Type argumentType) { + assertThat(ConstructorConstraintImpl.instance().satisfiedBy(argumentType)).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(ViolatedArgumentsProvider.class) + void testViolated(Type argumentType) { + assertThat(ConstructorConstraintImpl.instance().satisfiedBy(argumentType)).isFalse(); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintTest.java new file mode 100644 index 000000000..3937a703b --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/RecordConstraintTest.java @@ -0,0 +1,102 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.CLASS; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.RECORD; + +import au.com.integradev.delphi.type.factory.TypeFactoryImpl; +import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; +import au.com.integradev.delphi.utils.types.TypeFactoryUtils; +import au.com.integradev.delphi.utils.types.TypeMocker; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.sonar.plugins.communitydelphi.api.symbol.scope.DelphiScope; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +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 RecordConstraintTest { + private static final TypeFactory FACTORY = TypeFactoryUtils.defaultFactory(); + + private static class SatisfiedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + Type integer = FACTORY.getIntrinsic(IntrinsicType.INTEGER); + Type enumeration = ((TypeFactoryImpl) FACTORY).enumeration("", DelphiScope.unknownScope()); + return Stream.of( + Arguments.of(integer), + Arguments.of(enumeration), + Arguments.of(FACTORY.subrange("", integer)), + Arguments.of(FACTORY.subrange("", enumeration)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.BOOLEAN)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.DOUBLE)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.ANSICHAR)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.WIDECHAR)), + Arguments.of(TypeMocker.struct("TFoo", RECORD)), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(RecordConstraintImpl.instance())))); + } + } + + private static class ViolatedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.STRING)), + Arguments.of(TypeMocker.struct("TBar", CLASS)), + Arguments.of(TypeParameterTypeImpl.create("T")), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(mock(Constraint.class)))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(ClassConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of(ClassConstraintImpl.instance(), ConstructorConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + RecordConstraintImpl.instance(), + ConstructorConstraintImpl.instance(), + ClassConstraintImpl.instance())))); + } + } + + @ParameterizedTest + @ArgumentsSource(SatisfiedArgumentsProvider.class) + void testSatisfied(Type argumentType) { + assertThat(RecordConstraintImpl.instance().satisfiedBy(argumentType)).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(ViolatedArgumentsProvider.class) + void testViolated(Type argumentType) { + assertThat(RecordConstraintImpl.instance().satisfiedBy(argumentType)).isFalse(); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintTest.java new file mode 100644 index 000000000..2c9127576 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/generic/constraint/TypeConstraintTest.java @@ -0,0 +1,105 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2025 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.type.generic.constraint; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.CLASS; +import static org.sonar.plugins.communitydelphi.api.type.StructKind.RECORD; + +import au.com.integradev.delphi.type.generic.TypeParameterTypeImpl; +import au.com.integradev.delphi.utils.types.TypeFactoryUtils; +import au.com.integradev.delphi.utils.types.TypeMocker; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.sonar.plugins.communitydelphi.api.type.Constraint; +import org.sonar.plugins.communitydelphi.api.type.Constraint.TypeConstraint; +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 TypeConstraintTest { + private static final TypeFactory FACTORY = TypeFactoryUtils.defaultFactory(); + + private static final Type TFOO = TypeMocker.struct("TFoo", CLASS); + private static final Type TBAR = TypeMocker.struct("TBar", CLASS, TFOO); + private static final Type TBAZ = TypeMocker.struct("TBaz", CLASS, TBAR); + private static final TypeConstraint CONSTRAINT = new TypeConstraintImpl(TBAR); + + private static class SatisfiedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(TBAR), + Arguments.of(TBAZ), + Arguments.of(FACTORY.classOf(null, TBAR)), + Arguments.of(FACTORY.classOf(null, TBAZ)), + Arguments.of(TBAR), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + new TypeConstraintImpl(TypeMocker.struct("TBar", CLASS)), + ClassConstraintImpl.instance(), + ConstructorConstraintImpl.instance())))); + } + } + + private static class ViolatedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.BYTE)), + Arguments.of(FACTORY.getIntrinsic(IntrinsicType.STRING)), + Arguments.of(TFOO), + Arguments.of(TypeMocker.struct("TFlarp", RECORD)), + Arguments.of(TypeMocker.struct("TFlimFlam", CLASS)), + Arguments.of(TypeParameterTypeImpl.create("T")), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(mock(Constraint.class)))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(ClassConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create("T", List.of(ConstructorConstraintImpl.instance()))), + Arguments.of(TypeParameterTypeImpl.create("T", List.of(RecordConstraintImpl.instance()))), + Arguments.of( + TypeParameterTypeImpl.create( + "T", + List.of( + ClassConstraintImpl.instance(), + ConstructorConstraintImpl.instance(), + RecordConstraintImpl.instance())))); + } + } + + @ParameterizedTest + @ArgumentsSource(SatisfiedArgumentsProvider.class) + void testSatisfied(Type argumentType) { + assertThat(CONSTRAINT.satisfiedBy(argumentType)).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(ViolatedArgumentsProvider.class) + void testViolated(Type argumentType) { + assertThat(CONSTRAINT.satisfiedBy(argumentType)).isFalse(); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/utils/types/TypeMocker.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/utils/types/TypeMocker.java index d5f9bb52e..8e2262024 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/utils/types/TypeMocker.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/utils/types/TypeMocker.java @@ -22,9 +22,9 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.sonar.plugins.communitydelphi.api.symbol.scope.DelphiScope.unknownScope; import static org.sonar.plugins.communitydelphi.api.type.TypeFactory.unknownType; +import au.com.integradev.delphi.symbol.scope.TypeScopeImpl; import au.com.integradev.delphi.type.factory.HelperTypeImpl; import au.com.integradev.delphi.type.factory.StructTypeImpl; import org.sonar.plugins.communitydelphi.api.type.Parameter; @@ -53,10 +53,15 @@ public static StructType struct(String image, StructKind kind, Type parent) { type = mock(StructTypeImpl.class); } - when(type.typeScope()).thenReturn(unknownScope()); + var typeScope = new TypeScopeImpl(); + typeScope.setType(type); + + when(type.typeScope()).thenReturn(typeScope); when(type.isStruct()).thenReturn(true); + when(type.isClass()).thenReturn(kind == StructKind.CLASS); when(type.isInterface()).thenReturn(kind == StructKind.INTERFACE); when(type.isRecord()).thenReturn(kind == StructKind.RECORD); + when(type.isClass()).thenReturn(kind == StructKind.CLASS); when(type.isHelper()) .thenReturn(kind == StructKind.RECORD_HELPER || kind == StructKind.CLASS_HELPER); when(type.getImage()).thenReturn(image); diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/ImplicitSpecialization.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/ImplicitSpecialization.pas index e87100bce..616495767 100644 --- a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/ImplicitSpecialization.pas +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/ImplicitSpecialization.pas @@ -4,14 +4,16 @@ interface type TConsumable = class - constructor Create; override; + constructor Create; procedure GetEaten; class function Pizza: TConsumable; end; TConsumer = class - procedure Test(Consumable: TConsumable); - procedure Consume(Tasty: T); + procedure Test(Consumable: TConsumable); overload; + procedure Test(Consumables: array of TConsumable); overload; + procedure Consume(Consumable: T); overload; + procedure Consume(Consumables: array of T); overload; end; implementation @@ -21,6 +23,15 @@ procedure TConsumer.Consume(Consumable: T); Consumable.GetEaten; end; +procedure TConsumer.Consume(Consumables: array of T); +var + Consumable: T; +begin + for Consumable in Consumables do begin + Consume(Consumable); + end; +end; + procedure TConsumer.Test(Consumable: TConsumable); begin Consume(Consumable); @@ -28,4 +39,15 @@ procedure TConsumer.Test(Consumable: TConsumable); Consume(TConsumable.Create); end; +procedure TConsumer.Test(Consumables: array of TConsumable); +var + DynamicConsumables: array of TConsumable; +begin + DynamicConsumables := [TConsumable.Create]; + + Consume(Consumables); + Consume(DynamicConsumables); + Consume([TConsumable.Pizza]); +end; + end. \ No newline at end of file diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/TypeParameterConstructor.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/TypeParameterConstructor.pas new file mode 100644 index 000000000..6be32be76 --- /dev/null +++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/generics/TypeParameterConstructor.pas @@ -0,0 +1,28 @@ +unit TypeParameterConstructor; + +interface + +implementation + +type + TFoo = class + public + class function Bar: Boolean; + end; + + TTest = class + class function Test: Boolean; + end; + +class function TFoo.Bar: Boolean; +begin + Result := True; +end; + +class function TTest.Test: Boolean; +begin + Result := T.Create.Bar; + Result := T.Create().Bar; +end; + +end. \ No newline at end of file