From 9091b86a166855e6492f264800ba4c2acfcb437e Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 31 Jan 2026 02:54:11 +0000 Subject: [PATCH 01/14] Add support for Generics and This types to the LanguageSpecification project --- .../dsl/IdentifierValidator.java | 26 + .../dsl/LanguageSpecification.java | 83 + .../dsl/types/ParameterizedType.java | 128 ++ .../specification/dsl/types/ThisType.java | 68 + .../lara/langspec/thistype/actionModel.xml | 166 ++ .../lara/langspec/thistype/artifacts.xml | 159 ++ .../lara/langspec/thistype/joinPointModel.xml | 29 + .../dsl/IdentifierValidatorTest.java | 142 ++ .../dsl/types/ParameterizedTypeTest.java | 303 ++++ .../specification/dsl/types/ThisTypeTest.java | 147 ++ .../LanguageSpecificationIntegrationTest.java | 216 +++ .../langspec/ThisTypeXmlIntegrationTest.java | 1367 +++++++++++++++++ 12 files changed, 2834 insertions(+) create mode 100644 LanguageSpecification/src/org/lara/language/specification/dsl/types/ParameterizedType.java create mode 100644 LanguageSpecification/src/org/lara/language/specification/dsl/types/ThisType.java create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/actionModel.xml create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/artifacts.xml create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/joinPointModel.xml create mode 100644 LanguageSpecification/test/org/lara/language/specification/dsl/IdentifierValidatorTest.java create mode 100644 LanguageSpecification/test/org/lara/language/specification/dsl/types/ParameterizedTypeTest.java create mode 100644 LanguageSpecification/test/org/lara/language/specification/dsl/types/ThisTypeTest.java create mode 100644 LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/IdentifierValidator.java b/LanguageSpecification/src/org/lara/language/specification/dsl/IdentifierValidator.java index 227035a57..ca38325a4 100644 --- a/LanguageSpecification/src/org/lara/language/specification/dsl/IdentifierValidator.java +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/IdentifierValidator.java @@ -1,12 +1,22 @@ package org.lara.language.specification.dsl; +import org.lara.language.specification.dsl.types.ThisType; import org.lara.language.specification.exception.LanguageSpecificationException; +import java.util.Set; + /** * Central place for identifier validation shared across the language specification model. */ public final class IdentifierValidator { + /** + * Reserved keywords that cannot be used as identifiers for join points, attributes, etc. + */ + private static final Set RESERVED_KEYWORDS = Set.of( + ThisType.THIS_KEYWORD // 'this' is reserved for late-bound self type + ); + private IdentifierValidator() { } @@ -15,6 +25,12 @@ public static void requireValid(String identifier, String context) { return; } + if (RESERVED_KEYWORDS.contains(identifier)) { + throw new LanguageSpecificationException( + "Identifier '" + identifier + "' for " + context + + " is a reserved keyword and cannot be used"); + } + if (!isValidJavaLikeIdentifier(identifier)) { throw new LanguageSpecificationException( "Identifier '" + identifier + "' for " + context @@ -22,6 +38,16 @@ public static void requireValid(String identifier, String context) { } } + /** + * Checks if the given identifier is a reserved keyword. + * + * @param identifier the identifier to check + * @return true if it's a reserved keyword + */ + public static boolean isReservedKeyword(String identifier) { + return identifier != null && RESERVED_KEYWORDS.contains(identifier); + } + private static boolean isValidJavaLikeIdentifier(String identifier) { int firstCodePoint = identifier.codePointAt(0); if (!Character.isJavaIdentifierStart(firstCodePoint)) { diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java b/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java index 9ea7f4084..979272418 100644 --- a/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java @@ -251,6 +251,16 @@ public IType getType(String type) { return new ArrayType(baseType, arrayDimension); } + // Handle 'this' type (late-bound self type) + if (type.equals(ThisType.THIS_KEYWORD)) { + return ThisType.getInstance(); + } + + // Handle generic/parameterized types (e.g., List, Map) + if (type.contains("<") && type.contains(">")) { + return parseParameterizedType(type); + } + if (type.equalsIgnoreCase("template")) { return PrimitiveClasses.STRING; } @@ -279,6 +289,79 @@ public IType getType(String type) { throw new RuntimeException("Type given does not exist: " + type); } + /** + * Parses a parameterized type string (e.g., "List<String>", "Map<String, this>"). + * Supports nested generics and the 'this' type anywhere in the type arguments. + * + * @param type the type string with generic syntax + * @return a ParameterizedType representing the parsed type + */ + private IType parseParameterizedType(String type) { + int angleBracketStart = type.indexOf('<'); + int angleBracketEnd = type.lastIndexOf('>'); + + if (angleBracketStart == -1 || angleBracketEnd == -1 || angleBracketEnd <= angleBracketStart) { + throw new RuntimeException("Invalid parameterized type format: " + type); + } + + String baseTypeName = type.substring(0, angleBracketStart).trim(); + String argsString = type.substring(angleBracketStart + 1, angleBracketEnd).trim(); + + // Parse the base type + IType baseType = getBaseTypeForParameterized(baseTypeName); + + // Parse type arguments, respecting nested generics + List typeArguments = parseTypeArguments(argsString); + + return new ParameterizedType(baseType, typeArguments); + } + + /** + * Gets the base type for a parameterized type. For unknown base types, + * returns a GenericType to preserve the original string. + */ + private IType getBaseTypeForParameterized(String baseTypeName) { + // Check if it's a known primitive class (e.g., Map, List in some contexts) + if (PrimitiveClasses.contains(StringUtils.firstCharToUpper(baseTypeName))) { + return PrimitiveClasses.get(baseTypeName); + } + // For unknown base types (like List, Set, etc.), use GenericType as a wrapper + return new GenericType(baseTypeName, false); + } + + /** + * Parses a comma-separated list of type arguments, respecting nested generics. + * + * @param argsString the string containing type arguments (e.g., "String, Map<K, V>") + * @return list of parsed IType objects + */ + private List parseTypeArguments(String argsString) { + List arguments = new ArrayList<>(); + int depth = 0; + int start = 0; + + for (int i = 0; i < argsString.length(); i++) { + char c = argsString.charAt(i); + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (c == ',' && depth == 0) { + String arg = argsString.substring(start, i).trim(); + arguments.add(getType(arg)); + start = i + 1; + } + } + + // Add the last argument + String lastArg = argsString.substring(start).trim(); + if (!lastArg.isEmpty()) { + arguments.add(getType(lastArg)); + } + + return arguments; + } + public JoinPointClass getRoot() { return root; } diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/types/ParameterizedType.java b/LanguageSpecification/src/org/lara/language/specification/dsl/types/ParameterizedType.java new file mode 100644 index 000000000..6cfa0e9fc --- /dev/null +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/types/ParameterizedType.java @@ -0,0 +1,128 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Represents a parameterized (generic) type with a base type and type arguments. + * + *

Examples: + *

    + *
  • {@code List} - base type "List" with one type argument (String)
  • + *
  • {@code Map} - base type "Map" with two type arguments
  • + *
  • {@code List>} - nested parameterized types
  • + *
+ *

+ * + *

Type arguments can be any IType, including ThisType, other ParameterizedTypes, + * primitives, join point types, etc.

+ */ +public class ParameterizedType implements IType { + + private final IType baseType; + private final List typeArguments; + + /** + * Creates a parameterized type with the given base type and type arguments. + * + * @param baseType the base type (e.g., "List", "Map") + * @param typeArguments the list of type arguments; must not be null or empty + * @throws NullPointerException if baseType or typeArguments is null + * @throws IllegalArgumentException if typeArguments is empty + */ + public ParameterizedType(IType baseType, List typeArguments) { + Objects.requireNonNull(baseType, "Base type cannot be null"); + Objects.requireNonNull(typeArguments, "Type arguments cannot be null"); + if (typeArguments.isEmpty()) { + throw new IllegalArgumentException("Type arguments cannot be empty for a parameterized type"); + } + + this.baseType = baseType; + this.typeArguments = new ArrayList<>(typeArguments); + } + + /** + * Creates a parameterized type with a single type argument. + * + * @param baseType the base type + * @param typeArgument the single type argument + * @return the parameterized type + */ + public static ParameterizedType of(IType baseType, IType typeArgument) { + return new ParameterizedType(baseType, List.of(typeArgument)); + } + + /** + * Creates a parameterized type with multiple type arguments. + * + * @param baseType the base type + * @param typeArguments the type arguments + * @return the parameterized type + */ + public static ParameterizedType of(IType baseType, IType... typeArguments) { + return new ParameterizedType(baseType, List.of(typeArguments)); + } + + /** + * Returns the base type (e.g., for {@code List}, returns the type for "List"). + * + * @return the base type + */ + public IType getBaseType() { + return baseType; + } + + /** + * Returns an unmodifiable view of the type arguments. + * + * @return the list of type arguments + */ + public List getTypeArguments() { + return Collections.unmodifiableList(typeArguments); + } + + @Override + public String type() { + String args = typeArguments.stream() + .map(IType::toString) + .collect(Collectors.joining(", ")); + return baseType.type() + "<" + args + ">"; + } + + @Override + public String toString() { + return type(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ParameterizedType other)) { + return false; + } + return baseType.equals(other.baseType) && typeArguments.equals(other.typeArguments); + } + + @Override + public int hashCode() { + return Objects.hash(baseType, typeArguments); + } +} diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/types/ThisType.java b/LanguageSpecification/src/org/lara/language/specification/dsl/types/ThisType.java new file mode 100644 index 000000000..66a0035cc --- /dev/null +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/types/ThisType.java @@ -0,0 +1,68 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +/** + * Represents the late-bound 'this' type that refers to the current join point class. + * This type is never resolved within this project; resolution is deferred to downstream + * consumers (e.g., WeaverGenerator) where the owning join point context is known. + * + *

When used in attribute/action return types or parameters, 'this' provides polymorphic + * behavior across the join point hierarchy: if B extends A and foo() is defined in A with + * return type 'this', then A.foo() returns A and B.foo() returns B.

+ * + *

ThisType can appear standalone or as a type argument within generics + * (e.g., {@code List}, {@code Map}).

+ */ +public final class ThisType implements IType { + + /** The keyword used in type specifications to represent the self type. */ + public static final String THIS_KEYWORD = "this"; + + /** Singleton instance since ThisType has no state. */ + private static final ThisType INSTANCE = new ThisType(); + + private ThisType() { + // Private constructor to enforce singleton pattern + } + + /** + * Returns the singleton ThisType instance. + * + * @return the ThisType instance + */ + public static ThisType getInstance() { + return INSTANCE; + } + + @Override + public String type() { + return THIS_KEYWORD; + } + + @Override + public String toString() { + return THIS_KEYWORD; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ThisType; + } + + @Override + public int hashCode() { + return THIS_KEYWORD.hashCode(); + } +} diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/actionModel.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/actionModel.xml new file mode 100644 index 000000000..1822b7a70 --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/actionModel.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/artifacts.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/artifacts.xml new file mode 100644 index 000000000..bf3bbcd9d --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/artifacts.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/joinPointModel.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/joinPointModel.xml new file mode 100644 index 000000000..48d2aa5ea --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/thistype/joinPointModel.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + diff --git a/LanguageSpecification/test/org/lara/language/specification/dsl/IdentifierValidatorTest.java b/LanguageSpecification/test/org/lara/language/specification/dsl/IdentifierValidatorTest.java new file mode 100644 index 000000000..ad0ed0b87 --- /dev/null +++ b/LanguageSpecification/test/org/lara/language/specification/dsl/IdentifierValidatorTest.java @@ -0,0 +1,142 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.lara.language.specification.exception.LanguageSpecificationException; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("IdentifierValidator Tests") +class IdentifierValidatorTest { + + @Nested + @DisplayName("Reserved Keywords Tests") + class ReservedKeywordsTests { + + @Test + @DisplayName("Should reject 'this' as identifier") + void testRejectThis() { + LanguageSpecificationException exception = assertThrows( + LanguageSpecificationException.class, + () -> IdentifierValidator.requireValid("this", "join point name")); + + assertTrue(exception.getMessage().contains("reserved keyword")); + assertTrue(exception.getMessage().contains("this")); + } + + @Test + @DisplayName("isReservedKeyword should return true for 'this'") + void testIsReservedKeywordThis() { + assertTrue(IdentifierValidator.isReservedKeyword("this")); + } + + @Test + @DisplayName("isReservedKeyword should return false for normal identifiers") + void testIsReservedKeywordNormal() { + assertFalse(IdentifierValidator.isReservedKeyword("node")); + assertFalse(IdentifierValidator.isReservedKeyword("myAttribute")); + assertFalse(IdentifierValidator.isReservedKeyword("statement")); + } + + @Test + @DisplayName("isReservedKeyword should handle null") + void testIsReservedKeywordNull() { + assertFalse(IdentifierValidator.isReservedKeyword(null)); + } + } + + @Nested + @DisplayName("Valid Identifier Tests") + class ValidIdentifierTests { + + @Test + @DisplayName("Should accept valid Java-like identifiers") + void testValidIdentifiers() { + assertDoesNotThrow(() -> IdentifierValidator.requireValid("node", "test")); + assertDoesNotThrow(() -> IdentifierValidator.requireValid("myAttribute", "test")); + assertDoesNotThrow(() -> IdentifierValidator.requireValid("_privateField", "test")); + assertDoesNotThrow(() -> IdentifierValidator.requireValid("$special", "test")); + assertDoesNotThrow(() -> IdentifierValidator.requireValid("identifier123", "test")); + } + + @Test + @DisplayName("Should accept null identifier") + void testNullIdentifier() { + assertDoesNotThrow(() -> IdentifierValidator.requireValid(null, "test")); + } + + @Test + @DisplayName("Should accept empty identifier") + void testEmptyIdentifier() { + assertDoesNotThrow(() -> IdentifierValidator.requireValid("", "test")); + } + } + + @Nested + @DisplayName("Invalid Identifier Tests") + class InvalidIdentifierTests { + + @Test + @DisplayName("Should reject identifiers starting with digit") + void testRejectDigitStart() { + LanguageSpecificationException exception = assertThrows( + LanguageSpecificationException.class, + () -> IdentifierValidator.requireValid("123abc", "attribute name")); + + assertTrue(exception.getMessage().contains("Java identifier rules")); + } + + @Test + @DisplayName("Should reject identifiers with spaces") + void testRejectSpaces() { + assertThrows(LanguageSpecificationException.class, + () -> IdentifierValidator.requireValid("my attribute", "test")); + } + + @Test + @DisplayName("Should reject identifiers with special characters") + void testRejectSpecialChars() { + assertThrows(LanguageSpecificationException.class, + () -> IdentifierValidator.requireValid("my-attribute", "test")); + assertThrows(LanguageSpecificationException.class, + () -> IdentifierValidator.requireValid("my.attribute", "test")); + } + } + + @Nested + @DisplayName("JoinPointClass Integration Tests") + class JoinPointClassIntegrationTests { + + @Test + @DisplayName("Should not allow creating JoinPointClass with 'this' name") + void testJoinPointClassRejectsThis() { + LanguageSpecificationException exception = assertThrows( + LanguageSpecificationException.class, + () -> new JoinPointClass("this")); + + assertTrue(exception.getMessage().contains("reserved keyword")); + } + + @Test + @DisplayName("Should allow creating JoinPointClass with valid name") + void testJoinPointClassAcceptsValid() { + assertDoesNotThrow(() -> new JoinPointClass("node")); + assertDoesNotThrow(() -> new JoinPointClass("expression")); + assertDoesNotThrow(() -> new JoinPointClass("statement")); + } + } +} diff --git a/LanguageSpecification/test/org/lara/language/specification/dsl/types/ParameterizedTypeTest.java b/LanguageSpecification/test/org/lara/language/specification/dsl/types/ParameterizedTypeTest.java new file mode 100644 index 000000000..66c308566 --- /dev/null +++ b/LanguageSpecification/test/org/lara/language/specification/dsl/types/ParameterizedTypeTest.java @@ -0,0 +1,303 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("ParameterizedType Tests") +class ParameterizedTypeTest { + + @Nested + @DisplayName("Constructor Tests") + class ConstructorTests { + + @Test + @DisplayName("Should create with base type and single argument") + void testSingleArgument() { + IType baseType = new GenericType("List", false); + IType argType = PrimitiveClasses.STRING; + + ParameterizedType paramType = new ParameterizedType(baseType, List.of(argType)); + + assertEquals(baseType, paramType.getBaseType()); + assertEquals(1, paramType.getTypeArguments().size()); + assertEquals(argType, paramType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Should create with base type and multiple arguments") + void testMultipleArguments() { + IType baseType = new GenericType("Map", false); + IType keyType = PrimitiveClasses.STRING; + IType valueType = PrimitiveClasses.INTEGER; + + ParameterizedType paramType = new ParameterizedType(baseType, List.of(keyType, valueType)); + + assertEquals(baseType, paramType.getBaseType()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals(keyType, paramType.getTypeArguments().get(0)); + assertEquals(valueType, paramType.getTypeArguments().get(1)); + } + + @Test + @DisplayName("Should reject null base type") + void testNullBaseType() { + assertThrows(NullPointerException.class, () -> + new ParameterizedType(null, List.of(PrimitiveClasses.STRING))); + } + + @Test + @DisplayName("Should reject null type arguments") + void testNullTypeArguments() { + assertThrows(NullPointerException.class, () -> + new ParameterizedType(new GenericType("List", false), null)); + } + + @Test + @DisplayName("Should reject empty type arguments") + void testEmptyTypeArguments() { + assertThrows(IllegalArgumentException.class, () -> + new ParameterizedType(new GenericType("List", false), List.of())); + } + } + + @Nested + @DisplayName("Factory Method Tests") + class FactoryMethodTests { + + @Test + @DisplayName("of() with single argument") + void testOfSingleArg() { + IType baseType = new GenericType("List", false); + IType argType = PrimitiveClasses.STRING; + + ParameterizedType paramType = ParameterizedType.of(baseType, argType); + + assertEquals("List", paramType.toString()); + } + + @Test + @DisplayName("of() with varargs") + void testOfVarargs() { + IType baseType = new GenericType("Map", false); + + ParameterizedType paramType = ParameterizedType.of(baseType, + PrimitiveClasses.STRING, PrimitiveClasses.INTEGER); + + assertEquals("Map", paramType.toString()); + } + } + + @Nested + @DisplayName("Type Interface Implementation Tests") + class TypeInterfaceTests { + + @Test + @DisplayName("Should return correct type string for single argument") + void testTypeSingleArg() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertEquals("List", paramType.type()); + } + + @Test + @DisplayName("Should return correct type string for multiple arguments") + void testTypeMultipleArgs() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("Map", false), + PrimitiveClasses.STRING, PrimitiveClasses.INTEGER); + + assertEquals("Map", paramType.type()); + } + + @Test + @DisplayName("toString should match type()") + void testToStringMatchesType() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("Set", false), PrimitiveClasses.DOUBLE); + + assertEquals(paramType.type(), paramType.toString()); + } + + @Test + @DisplayName("Should not be an array type") + void testIsArrayFalse() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertFalse(paramType.isArray()); + } + } + + @Nested + @DisplayName("ThisType Integration Tests") + class ThisTypeIntegrationTests { + + @Test + @DisplayName("Should support ThisType as type argument") + void testThisTypeAsArgument() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("List", false), ThisType.getInstance()); + + assertEquals("List", paramType.toString()); + } + + @Test + @DisplayName("Should support ThisType mixed with other types") + void testThisTypeMixed() { + ParameterizedType paramType = ParameterizedType.of( + new GenericType("Map", false), + PrimitiveClasses.STRING, ThisType.getInstance()); + + assertEquals("Map", paramType.toString()); + } + + } + + @Nested + @DisplayName("Nested Generics Tests") + class NestedGenericsTests { + + @Test + @DisplayName("Should support nested parameterized types") + void testNestedParameterizedType() { + ParameterizedType innerType = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + ParameterizedType outerType = ParameterizedType.of( + new GenericType("Map", false), PrimitiveClasses.STRING, innerType); + + assertEquals("Map>", outerType.toString()); + } + + @Test + @DisplayName("Should handle deeply nested types") + void testDeeplyNestedTypes() { + ParameterizedType level1 = ParameterizedType.of( + new GenericType("Optional", false), ThisType.getInstance()); + ParameterizedType level2 = ParameterizedType.of( + new GenericType("List", false), level1); + ParameterizedType level3 = ParameterizedType.of( + new GenericType("Map", false), PrimitiveClasses.STRING, level2); + + assertEquals("Map>>", level3.toString()); + // Verify nested ThisType is accessible + ParameterizedType innerLevel2 = (ParameterizedType) level3.getTypeArguments().get(1); + ParameterizedType innerLevel1 = (ParameterizedType) innerLevel2.getTypeArguments().get(0); + assertInstanceOf(ThisType.class, innerLevel1.getTypeArguments().get(0)); + } + } + + @Nested + @DisplayName("Equality Tests") + class EqualityTests { + + @Test + @DisplayName("Should be equal with same base and arguments") + void testEquals() { + ParameterizedType type1 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + ParameterizedType type2 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertEquals(type1, type2); + } + + @Test + @DisplayName("Should not be equal with different base types") + void testNotEqualsDifferentBase() { + ParameterizedType type1 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + ParameterizedType type2 = ParameterizedType.of( + new GenericType("Set", false), PrimitiveClasses.STRING); + + assertNotEquals(type1, type2); + } + + @Test + @DisplayName("Should not be equal with different arguments") + void testNotEqualsDifferentArgs() { + ParameterizedType type1 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + ParameterizedType type2 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.INTEGER); + + assertNotEquals(type1, type2); + } + + @Test + @DisplayName("Should have consistent hashCode") + void testHashCode() { + ParameterizedType type1 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + ParameterizedType type2 = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertEquals(type1.hashCode(), type2.hashCode()); + } + + @Test + @DisplayName("Should not be equal to null") + void testNotEqualsNull() { + ParameterizedType type = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertNotEquals(null, type); + } + + @Test + @DisplayName("Should not be equal to other types") + void testNotEqualsOtherType() { + ParameterizedType type = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertNotEquals(type, new GenericType("List", false)); + } + } + + @Nested + @DisplayName("Immutability Tests") + class ImmutabilityTests { + + @Test + @DisplayName("Type arguments list should be immutable") + void testTypeArgumentsImmutable() { + ParameterizedType type = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertThrows(UnsupportedOperationException.class, () -> + type.getTypeArguments().add(PrimitiveClasses.INTEGER)); + } + } + + @Nested + @DisplayName("IType Contract Tests") + class ITypeContractTests { + + @Test + @DisplayName("Should implement IType interface") + void testImplementsIType() { + ParameterizedType type = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + + assertInstanceOf(IType.class, type); + } + } +} diff --git a/LanguageSpecification/test/org/lara/language/specification/dsl/types/ThisTypeTest.java b/LanguageSpecification/test/org/lara/language/specification/dsl/types/ThisTypeTest.java new file mode 100644 index 000000000..1a631bd73 --- /dev/null +++ b/LanguageSpecification/test/org/lara/language/specification/dsl/types/ThisTypeTest.java @@ -0,0 +1,147 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("ThisType Tests") +class ThisTypeTest { + + @Nested + @DisplayName("Singleton Pattern Tests") + class SingletonTests { + + @Test + @DisplayName("Should return the same instance") + void testSingletonInstance() { + ThisType first = ThisType.getInstance(); + ThisType second = ThisType.getInstance(); + + assertSame(first, second); + } + + @Test + @DisplayName("Should not be null") + void testNotNull() { + assertNotNull(ThisType.getInstance()); + } + } + + @Nested + @DisplayName("Type Interface Implementation Tests") + class TypeInterfaceTests { + + @Test + @DisplayName("Should return 'this' as type string") + void testType() { + assertEquals("this", ThisType.getInstance().type()); + } + + @Test + @DisplayName("Should return 'this' from toString") + void testToString() { + assertEquals("this", ThisType.getInstance().toString()); + } + + @Test + @DisplayName("Should not be an array type") + void testIsArrayFalse() { + assertFalse(ThisType.getInstance().isArray()); + } + } + + @Nested + @DisplayName("Keyword Constant Tests") + class KeywordTests { + + @Test + @DisplayName("THIS_KEYWORD should be 'this'") + void testThisKeyword() { + assertEquals("this", ThisType.THIS_KEYWORD); + } + + @Test + @DisplayName("type() should match THIS_KEYWORD") + void testTypeMatchesKeyword() { + assertEquals(ThisType.THIS_KEYWORD, ThisType.getInstance().type()); + } + } + + @Nested + @DisplayName("Equality Tests") + class EqualityTests { + + @Test + @DisplayName("Should be equal to itself") + void testEqualsItself() { + ThisType thisType = ThisType.getInstance(); + assertEquals(thisType, thisType); + } + + @Test + @DisplayName("Two instances should be equal") + void testEqualsTwoInstances() { + // Even though it's a singleton, test the equals implementation + assertEquals(ThisType.getInstance(), ThisType.getInstance()); + } + + @Test + @DisplayName("Should not be equal to null") + void testNotEqualsNull() { + assertNotEquals(null, ThisType.getInstance()); + } + + @Test + @DisplayName("Should not be equal to other types") + void testNotEqualsOtherTypes() { + assertNotEquals(ThisType.getInstance(), new GenericType("this", false)); + assertNotEquals(ThisType.getInstance(), "this"); + } + + @Test + @DisplayName("Should have consistent hashCode") + void testHashCode() { + int hash1 = ThisType.getInstance().hashCode(); + int hash2 = ThisType.getInstance().hashCode(); + + assertEquals(hash1, hash2); + assertEquals("this".hashCode(), hash1); + } + } + + @Nested + @DisplayName("IType Contract Tests") + class ITypeContractTests { + + @Test + @DisplayName("Should implement IType interface") + void testImplementsIType() { + assertInstanceOf(IType.class, ThisType.getInstance()); + } + + @Test + @DisplayName("Should be usable as IType") + void testUsableAsIType() { + IType type = ThisType.getInstance(); + + assertEquals("this", type.type()); + assertEquals("this", type.toString()); + assertFalse(type.isArray()); + } + } +} diff --git a/LanguageSpecification/test/org/lara/language/specification/integration/LanguageSpecificationIntegrationTest.java b/LanguageSpecification/test/org/lara/language/specification/integration/LanguageSpecificationIntegrationTest.java index 59722de35..d1a121040 100644 --- a/LanguageSpecification/test/org/lara/language/specification/integration/LanguageSpecificationIntegrationTest.java +++ b/LanguageSpecification/test/org/lara/language/specification/integration/LanguageSpecificationIntegrationTest.java @@ -8,6 +8,9 @@ import org.lara.language.specification.dsl.JoinPointClass; import org.lara.language.specification.dsl.Action; import org.lara.language.specification.dsl.Attribute; +import org.lara.language.specification.dsl.types.IType; +import org.lara.language.specification.dsl.types.ThisType; +import org.lara.language.specification.dsl.types.ParameterizedType; import java.io.File; import java.io.FileWriter; @@ -340,4 +343,217 @@ void testCompleteWorkflow() { assertNotNull(statement.toString()); assertFalse(statement.toString().trim().isEmpty()); } + + // ==================== ThisType and Generics Integration Tests ==================== + + @Test + void testThisTypeInAttributeReturnType() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'clone' attribute which returns 'this' + Attribute cloneAttr = node.getAttributesSelf().stream() + .filter(a -> "clone".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(cloneAttr, "Should have 'clone' attribute"); + IType returnType = cloneAttr.getType(); + + // Should be ThisType, not resolved to JPType + assertInstanceOf(ThisType.class, returnType); + assertEquals("this", returnType.toString()); + } + + @Test + void testThisTypeInActionReturnType() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'copy' action which returns 'this' + Action copyAction = node.getActionsSelf().stream() + .filter(a -> "copy".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(copyAction, "Should have 'copy' action"); + assertEquals("this", copyAction.getReturnType()); + } + + @Test + void testThisTypeInActionParameter() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'merge' action which has 'this' as parameter type + Action mergeAction = node.getActionsSelf().stream() + .filter(a -> "merge".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(mergeAction, "Should have 'merge' action"); + assertEquals(1, mergeAction.getParameters().size()); + assertEquals("this", mergeAction.getParameters().get(0).getType()); + } + + @Test + void testGenericTypeWithThisArgument() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'children' attribute which returns 'List' + Attribute childrenAttr = node.getAttributesSelf().stream() + .filter(a -> "children".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(childrenAttr, "Should have 'children' attribute"); + IType returnType = childrenAttr.getType(); + + // Should be ParameterizedType with ThisType argument + assertInstanceOf(ParameterizedType.class, returnType); + ParameterizedType paramType = (ParameterizedType) returnType; + assertEquals("List", paramType.toString()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(0)); + } + + @Test + void testComplexGenericWithThis() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'metadata' attribute which returns 'Map' + Attribute metadataAttr = node.getAttributesSelf().stream() + .filter(a -> "metadata".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(metadataAttr, "Should have 'metadata' attribute"); + IType returnType = metadataAttr.getType(); + + // Should be ParameterizedType: Map + assertInstanceOf(ParameterizedType.class, returnType); + ParameterizedType paramType = (ParameterizedType) returnType; + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(1)); + } + + @Test + void testNestedGenericWithThis() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(node); + + // Find the 'nestedChildren' attribute which returns 'List>' + Attribute nestedAttr = node.getAttributesSelf().stream() + .filter(a -> "nestedChildren".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(nestedAttr, "Should have 'nestedChildren' attribute"); + IType returnType = nestedAttr.getType(); + + // Should be ParameterizedType: List> + assertInstanceOf(ParameterizedType.class, returnType); + ParameterizedType outerType = (ParameterizedType) returnType; + assertEquals("List>", outerType.toString()); + + // Inner type should also be ParameterizedType + IType innerType = outerType.getTypeArguments().get(0); + assertInstanceOf(ParameterizedType.class, innerType); + ParameterizedType innerParamType = (ParameterizedType) innerType; + assertInstanceOf(ThisType.class, innerParamType.getTypeArguments().get(0)); + } + + @Test + void testThisTypePreservedInInheritedAttribute() throws IOException { + File specDir = createSpecWithThisType(); + LanguageSpecification langSpec = LanguageSpecification.newInstance(specDir); + + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointClass expr = langSpec.getJoinPoint("expr"); + assertNotNull(node); + assertNotNull(expr); + + // expr extends node, so it should inherit the 'clone' attribute + // The 'this' type should still be ThisType (late-bound) + Attribute exprClone = expr.getAttributes().stream() + .filter(a -> "clone".equals(a.getName())) + .findFirst() + .orElse(null); + + assertNotNull(exprClone, "expr should inherit 'clone' attribute from node"); + IType returnType = exprClone.getType(); + + // Should still be ThisType, not resolved to 'expr' or 'node' + assertInstanceOf(ThisType.class, returnType); + assertEquals("this", returnType.toString()); + } + + /** + * Creates a language specification with 'this' type usage. + */ + private File createSpecWithThisType() throws IOException { + File specDir = tempDir.resolve("this-type-spec").toFile(); + specDir.mkdirs(); + + // Create joinPointModel.xml + try (FileWriter writer = new FileWriter(new File(specDir, "joinPointModel.xml"))) { + writer.write("\n"); + writer.write("\n"); + writer.write(" \n"); + writer.write(" \n"); + writer.write("\n"); + } + + // Create artifacts.xml with 'this' and generic types + try (FileWriter writer = new FileWriter(new File(specDir, "artifacts.xml"))) { + writer.write("\n"); + writer.write("\n"); + writer.write(" \n"); + // Attribute returning 'this' + writer.write(" \n"); + // Attribute returning List + writer.write(" \n"); + // Attribute returning Map + writer.write(" \n"); + // Attribute returning nested generic List> + writer.write(" \n"); + writer.write(" \n"); + writer.write("\n"); + } + + // Create actionModel.xml with 'this' in return and parameter types + try (FileWriter writer = new FileWriter(new File(specDir, "actionModel.xml"))) { + writer.write("\n"); + writer.write("\n"); + // Action returning 'this' + writer.write(" \n"); + // Action with 'this' as parameter type + writer.write(" \n"); + writer.write(" \n"); + writer.write(" \n"); + writer.write("\n"); + } + + return specDir; + } } diff --git a/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java new file mode 100644 index 000000000..ff319447c --- /dev/null +++ b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java @@ -0,0 +1,1367 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.lara.langspec; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.lara.language.specification.ast.ActionNode; +import org.lara.language.specification.ast.AttributeNode; +import org.lara.language.specification.ast.DeclarationNode; +import org.lara.language.specification.ast.JoinPointNode; +import org.lara.language.specification.ast.LangSpecNode; +import org.lara.language.specification.ast.NodeFactory; +import org.lara.language.specification.ast.RootNode; +import org.lara.language.specification.ast.TypeDefNode; +import org.lara.language.specification.dsl.Action; +import org.lara.language.specification.dsl.Attribute; +import org.lara.language.specification.dsl.JoinPointClass; +import org.lara.language.specification.dsl.LanguageSpecification; +import org.lara.language.specification.dsl.Parameter; +import org.lara.language.specification.dsl.types.ArrayType; +import org.lara.language.specification.dsl.types.IType; +import org.lara.language.specification.dsl.types.ParameterizedType; +import org.lara.language.specification.dsl.types.ThisType; +import org.lara.language.specification.dsl.types.TypeDef; +import pt.up.fe.specs.util.SpecsSystem; +import pt.up.fe.specs.util.providers.ResourceProvider; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Comprehensive integration tests for the 'this' type and generic types feature. + * + *

Tests cover: + *

    + *
  • 'this' as attribute/action return types
  • + *
  • 'this' as action parameter types
  • + *
  • Simple generics (List<String>, Map<K,V>)
  • + *
  • Generics with 'this' (List<this>, Map<String, this>)
  • + *
  • Nested generics (List<List<this>>)
  • + *
  • Arrays of 'this' (this[])
  • + *
  • Inheritance preservation of 'this' type
  • + *
  • AST/JSON output verification
  • + *
  • TypeDef with generics and 'this'
  • + *
+ */ +@DisplayName("ThisType and Generics XML Integration Tests") +public class ThisTypeXmlIntegrationTest { + + private static final String BASE_PACKAGE = "pt/up/fe/specs/lara/langspec/thistype/"; + + /** + * Resource provider enum for thistype test resources. + */ + public enum ThisTypeTestResource implements ResourceProvider { + JOIN_POINT_MODEL("joinPointModel.xml"), + ATTRIBUTE_MODEL("artifacts.xml"), + ACTION_MODEL("actionModel.xml"); + + private final String resource; + + ThisTypeTestResource(String resource) { + this.resource = BASE_PACKAGE + resource; + } + + @Override + public String getResource() { + return resource; + } + } + + private LanguageSpecification langSpec; + + @BeforeAll + static void initSystem() { + SpecsSystem.programStandardInit(); + } + + @BeforeEach + void setUp() { + langSpec = LangSpecsXmlParser.parse( + ThisTypeTestResource.JOIN_POINT_MODEL.toStream(), + ThisTypeTestResource.ATTRIBUTE_MODEL.toStream(), + ThisTypeTestResource.ACTION_MODEL.toStream(), + true + ); + } + + // ==================== Basic Parsing Tests ==================== + + @Nested + @DisplayName("Basic Parsing and Structure Tests") + class BasicParsingTests { + + @Test + @DisplayName("Language specification parses successfully") + void testParsingSucceeds() { + assertNotNull(langSpec); + assertNotNull(langSpec.getRoot()); + assertNotNull(langSpec.getGlobal()); + } + + @Test + @DisplayName("All join points are present") + void testJoinPointsPresent() { + assertNotNull(langSpec.getJoinPoint("node")); + assertNotNull(langSpec.getJoinPoint("expr")); + assertNotNull(langSpec.getJoinPoint("binaryExpr")); + assertNotNull(langSpec.getJoinPoint("stmt")); + assertNotNull(langSpec.getJoinPoint("loop")); + assertNotNull(langSpec.getJoinPoint("container")); + } + + @Test + @DisplayName("Inheritance hierarchy is correct") + void testInheritanceHierarchy() { + JoinPointClass expr = langSpec.getJoinPoint("expr"); + JoinPointClass binaryExpr = langSpec.getJoinPoint("binaryExpr"); + JoinPointClass stmt = langSpec.getJoinPoint("stmt"); + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + assertTrue(expr.getExtend().isPresent()); + assertEquals("node", expr.getExtend().get().getName()); + + assertTrue(binaryExpr.getExtend().isPresent()); + assertEquals("expr", binaryExpr.getExtend().get().getName()); + + assertTrue(stmt.getExtend().isPresent()); + assertEquals("node", stmt.getExtend().get().getName()); + + assertTrue(loop.getExtend().isPresent()); + assertEquals("stmt", loop.getExtend().get().getName()); + } + + @Test + @DisplayName("Root alias is correct") + void testRootAlias() { + assertEquals("root", langSpec.getRootAlias()); + assertEquals("node", langSpec.getRoot().getName()); + } + } + + // ==================== ThisType in Attributes Tests ==================== + + @Nested + @DisplayName("'this' Type in Attribute Return Types") + class ThisTypeAttributeTests { + + @Test + @DisplayName("Simple 'this' return type is parsed as ThisType") + void testSimpleThisReturnType() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute cloneAttr = findAttribute(node.getAttributesSelf(), "clone"); + + assertNotNull(cloneAttr); + assertInstanceOf(ThisType.class, cloneAttr.getType()); + assertEquals("this", cloneAttr.getType().toString()); + assertEquals("this", cloneAttr.getReturnType()); + } + + @Test + @DisplayName("Multiple 'this' attributes on same join point") + void testMultipleThisAttributes() { + JoinPointClass node = langSpec.getJoinPoint("node"); + + Attribute clone = findAttribute(node.getAttributesSelf(), "clone"); + Attribute parent = findAttribute(node.getAttributesSelf(), "parent"); + + assertInstanceOf(ThisType.class, clone.getType()); + assertInstanceOf(ThisType.class, parent.getType()); + + // Both should be the same singleton instance + assertSame(clone.getType(), parent.getType()); + } + + @Test + @DisplayName("'this' type with parameters") + void testThisWithParameters() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute ancestorOfType = findAttribute(node.getAttributesSelf(), "ancestorOfType"); + + assertNotNull(ancestorOfType); + assertInstanceOf(ThisType.class, ancestorOfType.getType()); + assertEquals(1, ancestorOfType.getParameters().size()); + assertEquals("typeName", ancestorOfType.getParameters().get(0).getName()); + assertEquals("String", ancestorOfType.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Global attribute with 'this' return type") + void testGlobalThisAttribute() { + JoinPointClass global = langSpec.getGlobal(); + Attribute rootAttr = findAttribute(global.getAttributesSelf(), "root"); + + assertNotNull(rootAttr); + assertInstanceOf(ThisType.class, rootAttr.getType()); + assertEquals("this", rootAttr.getType().toString()); + } + + @Test + @DisplayName("Array of 'this' type: this[]") + void testThisArrayType() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute children = findAttribute(node.getAttributesSelf(), "children"); + + assertNotNull(children); + assertInstanceOf(ArrayType.class, children.getType()); + ArrayType arrayType = (ArrayType) children.getType(); + assertInstanceOf(ThisType.class, arrayType.getBaseType()); + assertEquals("this[]", arrayType.toString()); + } + + @Test + @DisplayName("Multi-dimensional array of 'this': this[][]") + void testThisMultiDimensionalArray() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute childrenMatrix = findAttribute(node.getAttributesSelf(), "childrenMatrix"); + + assertNotNull(childrenMatrix); + assertInstanceOf(ArrayType.class, childrenMatrix.getType()); + assertEquals("this[][]", childrenMatrix.getType().toString()); + } + + @Test + @DisplayName("Global array of 'this' type") + void testGlobalThisArray() { + JoinPointClass global = langSpec.getGlobal(); + Attribute ancestors = findAttribute(global.getAttributesSelf(), "ancestors"); + + assertNotNull(ancestors); + assertInstanceOf(ArrayType.class, ancestors.getType()); + ArrayType arrayType = (ArrayType) ancestors.getType(); + assertInstanceOf(ThisType.class, arrayType.getBaseType()); + } + + @Test + @DisplayName("Attribute with 'this' as parameter type") + void testAttributeWithThisParameterType() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute findSimilar = findAttribute(node.getAttributesSelf(), "findSimilar"); + + assertNotNull(findSimilar, "Should have 'findSimilar' attribute"); + assertInstanceOf(ParameterizedType.class, findSimilar.getType()); + assertEquals("List", findSimilar.getType().toString()); + + // Verify parameter has 'this' type + assertEquals(1, findSimilar.getParameters().size()); + assertEquals("target", findSimilar.getParameters().get(0).getName()); + assertEquals("this", findSimilar.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Attribute with multiple 'this' parameters") + void testAttributeWithMultipleThisParameters() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute findBetween = findAttribute(node.getAttributesSelf(), "findBetween"); + + assertNotNull(findBetween, "Should have 'findBetween' attribute"); + assertEquals("List", findBetween.getType().toString()); + + // Verify both parameters have 'this' type + assertEquals(2, findBetween.getParameters().size()); + assertEquals("start", findBetween.getParameters().get(0).getName()); + assertEquals("this", findBetween.getParameters().get(0).getType()); + assertEquals("end", findBetween.getParameters().get(1).getName()); + assertEquals("this", findBetween.getParameters().get(1).getType()); + } + } + + // ==================== Generic Types Tests ==================== + + @Nested + @DisplayName("Generic Types Parsing") + class GenericTypesTests { + + @Test + @DisplayName("Simple generic: Map") + void testSimpleMapGeneric() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute attrs = findAttribute(node.getAttributesSelf(), "attributes"); + + assertNotNull(attrs); + assertInstanceOf(ParameterizedType.class, attrs.getType()); + ParameterizedType paramType = (ParameterizedType) attrs.getType(); + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + } + + @Test + @DisplayName("Simple generic: List") + void testSimpleListGeneric() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute tags = findAttribute(node.getAttributesSelf(), "tags"); + + assertNotNull(tags); + assertInstanceOf(ParameterizedType.class, tags.getType()); + ParameterizedType paramType = (ParameterizedType) tags.getType(); + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + } + + @Test + @DisplayName("Generic with Object type argument") + void testGenericWithObject() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute props = findAttribute(node.getAttributesSelf(), "properties"); + + assertNotNull(props); + assertInstanceOf(ParameterizedType.class, props.getType()); + assertEquals("Map", props.getType().toString()); + } + } + + // ==================== Generics with 'this' Tests ==================== + + @Nested + @DisplayName("Generics with 'this' Type Argument") + class GenericsWithThisTests { + + @Test + @DisplayName("List is parsed correctly") + void testListOfThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute childList = findAttribute(node.getAttributesSelf(), "childList"); + + assertNotNull(childList); + assertInstanceOf(ParameterizedType.class, childList.getType()); + ParameterizedType paramType = (ParameterizedType) childList.getType(); + + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Set is parsed correctly") + void testSetOfThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute descendantSet = findAttribute(node.getAttributesSelf(), "descendantSet"); + + assertNotNull(descendantSet); + assertInstanceOf(ParameterizedType.class, descendantSet.getType()); + ParameterizedType paramType = (ParameterizedType) descendantSet.getType(); + + assertEquals("Set", paramType.toString()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Map is parsed correctly") + void testMapWithThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute namedChildren = findAttribute(node.getAttributesSelf(), "namedChildren"); + + assertNotNull(namedChildren); + assertInstanceOf(ParameterizedType.class, namedChildren.getType()); + ParameterizedType paramType = (ParameterizedType) namedChildren.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + + // First arg should not be ThisType + assertNotEquals(ThisType.class, paramType.getTypeArguments().get(0).getClass()); + + // Second arg should be ThisType + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(1)); + } + + @Test + @DisplayName("Map is parsed correctly") + void testMapIntegerThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute indexedNodes = findAttribute(node.getAttributesSelf(), "indexedNodes"); + + assertNotNull(indexedNodes); + assertInstanceOf(ParameterizedType.class, indexedNodes.getType()); + assertEquals("Map", indexedNodes.getType().toString()); + } + + @Test + @DisplayName("Global List in attribute") + void testGlobalListOfThis() { + JoinPointClass global = langSpec.getGlobal(); + Attribute siblings = findAttribute(global.getAttributesSelf(), "siblings"); + + assertNotNull(siblings); + assertInstanceOf(ParameterizedType.class, siblings.getType()); + ParameterizedType paramType = (ParameterizedType) siblings.getType(); + assertEquals("List", paramType.toString()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(0)); + } + } + + // ==================== Nested Generics Tests ==================== + + @Nested + @DisplayName("Nested Generic Types with 'this'") + class NestedGenericsTests { + + @Test + @DisplayName("List> is parsed correctly") + void testNestedListOfThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute childGroups = findAttribute(node.getAttributesSelf(), "childGroups"); + + assertNotNull(childGroups); + assertInstanceOf(ParameterizedType.class, childGroups.getType()); + ParameterizedType outerType = (ParameterizedType) childGroups.getType(); + + assertEquals("List>", outerType.toString()); + assertEquals(1, outerType.getTypeArguments().size()); + + // Inner type should also be ParameterizedType + IType innerArg = outerType.getTypeArguments().get(0); + assertInstanceOf(ParameterizedType.class, innerArg); + ParameterizedType innerType = (ParameterizedType) innerArg; + + assertEquals("List", innerType.toString()); + assertInstanceOf(ThisType.class, innerType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Map> is parsed correctly") + void testMapWithListOfThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute categorizedNodes = findAttribute(node.getAttributesSelf(), "categorizedNodes"); + + assertNotNull(categorizedNodes); + assertInstanceOf(ParameterizedType.class, categorizedNodes.getType()); + ParameterizedType mapType = (ParameterizedType) categorizedNodes.getType(); + + assertEquals("Map>", mapType.toString()); + assertEquals(2, mapType.getTypeArguments().size()); + + // Second argument should be List + IType secondArg = mapType.getTypeArguments().get(1); + assertInstanceOf(ParameterizedType.class, secondArg); + ParameterizedType listType = (ParameterizedType) secondArg; + assertEquals("List", listType.toString()); + assertInstanceOf(ThisType.class, listType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Map> is parsed correctly") + void testDeeplyNestedMap() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute hierarchy = findAttribute(node.getAttributesSelf(), "hierarchy"); + + assertNotNull(hierarchy); + assertInstanceOf(ParameterizedType.class, hierarchy.getType()); + assertEquals("Map>", hierarchy.getType().toString()); + + ParameterizedType outerMap = (ParameterizedType) hierarchy.getType(); + ParameterizedType innerMap = (ParameterizedType) outerMap.getTypeArguments().get(1); + assertInstanceOf(ThisType.class, innerMap.getTypeArguments().get(1)); + } + + @Test + @DisplayName("Deeply nested: List>>") + void testVeryDeeplyNested() { + JoinPointClass container = langSpec.getJoinPoint("container"); + Attribute deepNested = findAttribute(container.getAttributesSelf(), "deepNested"); + + assertNotNull(deepNested); + assertInstanceOf(ParameterizedType.class, deepNested.getType()); + assertEquals("List>>", deepNested.getType().toString()); + + ParameterizedType paramType = (ParameterizedType) deepNested.getType(); + // Navigate to the innermost type: List -> Map -> List -> this + ParameterizedType mapType = (ParameterizedType) paramType.getTypeArguments().get(0); + ParameterizedType innerList = (ParameterizedType) mapType.getTypeArguments().get(1); + assertInstanceOf(ThisType.class, innerList.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Map with 'this' as both key and value") + void testMapThisToThis() { + JoinPointClass container = langSpec.getJoinPoint("container"); + Attribute multiThis = findAttribute(container.getAttributesSelf(), "multiThis"); + + assertNotNull(multiThis); + assertInstanceOf(ParameterizedType.class, multiThis.getType()); + ParameterizedType mapType = (ParameterizedType) multiThis.getType(); + + assertEquals("Map", mapType.toString()); + assertEquals(2, mapType.getTypeArguments().size()); + assertInstanceOf(ThisType.class, mapType.getTypeArguments().get(0)); + assertInstanceOf(ThisType.class, mapType.getTypeArguments().get(1)); + } + } + + // ==================== 'this' in Actions Tests ==================== + + @Nested + @DisplayName("'this' Type in Action Return and Parameters") + class ThisTypeActionTests { + + @Test + @DisplayName("Action with 'this' return type") + void testActionThisReturnType() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action copy = findAction(node.getActionsSelf(), "copy"); + + assertNotNull(copy); + assertEquals("this", copy.getReturnType()); + assertInstanceOf(ThisType.class, copy.getType()); + } + + @Test + @DisplayName("Global action with 'this' return type") + void testGlobalActionThisReturn() { + JoinPointClass global = langSpec.getGlobal(); + Action selfTransform = findAction(global.getActionsSelf(), "selfTransform"); + + assertNotNull(selfTransform); + assertEquals("this", selfTransform.getReturnType()); + assertInstanceOf(ThisType.class, selfTransform.getType()); + } + + @Test + @DisplayName("Action with 'this' as only parameter") + void testActionThisOnlyParameter() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action insertBefore = findAction(node.getActionsSelf(), "insertBefore"); + + assertNotNull(insertBefore); + assertEquals(1, insertBefore.getParameters().size()); + + Parameter param = insertBefore.getParameters().get(0); + assertEquals("node", param.getName()); + assertEquals("this", param.getType()); + } + + @Test + @DisplayName("Action with 'this' parameter returning 'this'") + void testActionThisParamAndReturn() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action merge = findAction(node.getActionsSelf(), "merge"); + + assertNotNull(merge); + assertEquals("this", merge.getReturnType()); + assertEquals(1, merge.getParameters().size()); + assertEquals("this", merge.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with 'this' mixed with other parameter types") + void testActionThisMixedWithOtherParams() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action swap = findAction(node.getActionsSelf(), "swap"); + + assertNotNull(swap); + assertEquals(2, swap.getParameters().size()); + assertEquals("this", swap.getParameters().get(0).getType()); + assertEquals("Boolean", swap.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Action with 'this' at different positions") + void testActionThisAtDifferentPositions() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action insertAt = findAction(node.getActionsSelf(), "insertAt"); + + assertNotNull(insertAt); + assertEquals(2, insertAt.getParameters().size()); + assertEquals("Integer", insertAt.getParameters().get(0).getType()); + assertEquals("this", insertAt.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Action with multiple 'this' parameters") + void testActionMultipleThisParams() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action replaceBetween = findAction(node.getActionsSelf(), "replaceBetween"); + + assertNotNull(replaceBetween); + assertEquals(3, replaceBetween.getParameters().size()); + assertEquals("this", replaceBetween.getParameters().get(0).getType()); + assertEquals("this", replaceBetween.getParameters().get(1).getType()); + assertEquals("this", replaceBetween.getParameters().get(2).getType()); + } + + @Test + @DisplayName("Action with 'this' in both parameter and return with generic") + void testActionBinaryExprSetOperands() { + JoinPointClass binaryExpr = langSpec.getJoinPoint("binaryExpr"); + Action setOperands = findAction(binaryExpr.getActionsSelf(), "setOperands"); + + assertNotNull(setOperands); + assertEquals("void", setOperands.getReturnType()); + assertEquals(2, setOperands.getParameters().size()); + assertEquals("this", setOperands.getParameters().get(0).getType()); + assertEquals("this", setOperands.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Global action with 'this' as parameter") + void testGlobalActionThisParameter() { + JoinPointClass global = langSpec.getGlobal(); + Action replaceWith = findAction(global.getActionsSelf(), "replaceWith"); + + assertNotNull(replaceWith); + assertEquals("void", replaceWith.getReturnType()); + assertEquals(1, replaceWith.getParameters().size()); + assertEquals("this", replaceWith.getParameters().get(0).getType()); + } + } + + // ==================== Actions with Generics Tests ==================== + + @Nested + @DisplayName("Action Return Types and Parameters with Generics") + class ActionGenericsTests { + + @Test + @DisplayName("Action returning List") + void testActionReturningListOfThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action findChildren = findAction(node.getActionsSelf(), "findChildren"); + + assertNotNull(findChildren); + assertEquals("List", findChildren.getReturnType()); + assertInstanceOf(ParameterizedType.class, findChildren.getType()); + assertInstanceOf(ThisType.class, ((ParameterizedType) findChildren.getType()).getTypeArguments().get(0)); + } + + @Test + @DisplayName("Action returning Map>") + void testActionReturningNestedGeneric() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action groupByType = findAction(node.getActionsSelf(), "groupByType"); + + assertNotNull(groupByType); + assertEquals("Map>", groupByType.getReturnType()); + } + + @Test + @DisplayName("Action with List parameter") + void testActionWithListOfThisParam() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action insertAll = findAction(node.getActionsSelf(), "insertAll"); + + assertNotNull(insertAll); + assertEquals(1, insertAll.getParameters().size()); + assertEquals("List", insertAll.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with Map parameter") + void testActionWithMapOfThisParam() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action replaceAll = findAction(node.getActionsSelf(), "replaceAll"); + + assertNotNull(replaceAll); + assertEquals(1, replaceAll.getParameters().size()); + assertEquals("Map", replaceAll.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action returning this[]") + void testActionReturningThisArray() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action toArray = findAction(node.getActionsSelf(), "toArray"); + + assertNotNull(toArray); + assertEquals("this[]", toArray.getReturnType()); + assertInstanceOf(ArrayType.class, toArray.getType()); + } + + @Test + @DisplayName("Global action returning List") + void testGlobalActionReturningListThis() { + JoinPointClass global = langSpec.getGlobal(); + Action findAll = findAction(global.getActionsSelf(), "findAll"); + + assertNotNull(findAll); + assertEquals("List", findAll.getReturnType()); + } + + @Test + @DisplayName("Global action returning Map") + void testGlobalActionReturningMapThis() { + JoinPointClass global = langSpec.getGlobal(); + Action categorize = findAction(global.getActionsSelf(), "categorize"); + + assertNotNull(categorize); + assertEquals("Map", categorize.getReturnType()); + } + + @Test + @DisplayName("Action returning simple generic Map") + void testActionReturningSimpleGeneric() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action getMetadata = findAction(node.getActionsSelf(), "getMetadata"); + + assertNotNull(getMetadata); + assertEquals("Map", getMetadata.getReturnType()); + assertInstanceOf(ParameterizedType.class, getMetadata.getType()); + // Verify no ThisType in arguments + ParameterizedType metadataType = (ParameterizedType) getMetadata.getType(); + assertFalse(metadataType.getTypeArguments().stream().anyMatch(t -> t instanceof ThisType)); + } + + @Test + @DisplayName("Action with complex nested generic parameter") + void testActionComplexNestedGenericParam() { + JoinPointClass container = langSpec.getJoinPoint("container"); + Action addToAll = findAction(container.getActionsSelf(), "addToAll"); + + assertNotNull(addToAll); + assertEquals(1, addToAll.getParameters().size()); + assertEquals("Map>", addToAll.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action returning List>") + void testActionReturningListOfMaps() { + JoinPointClass container = langSpec.getJoinPoint("container"); + Action processNested = findAction(container.getActionsSelf(), "processNested"); + + assertNotNull(processNested); + assertEquals("List>", processNested.getReturnType()); + } + } + + // ==================== Inheritance Tests ==================== + + @Nested + @DisplayName("'this' Type Preservation in Inheritance") + class InheritanceTests { + + @Test + @DisplayName("'this' type is preserved when attribute is inherited") + void testThisPreservedInInheritedAttribute() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointClass expr = langSpec.getJoinPoint("expr"); + + // Find clone on node (where it's defined) + Attribute cloneOnNode = findAttribute(node.getAttributesSelf(), "clone"); + assertNotNull(cloneOnNode); + assertInstanceOf(ThisType.class, cloneOnNode.getType()); + + // Find clone on expr (inherited) + Attribute cloneOnExpr = findAttribute(expr.getAttributes(), "clone"); + assertNotNull(cloneOnExpr); + + // Should still be ThisType, not resolved to 'node' or 'expr' + assertInstanceOf(ThisType.class, cloneOnExpr.getType()); + assertEquals("this", cloneOnExpr.getType().toString()); + + // Should be the same instance (singleton) + assertSame(cloneOnNode.getType(), cloneOnExpr.getType()); + } + + @Test + @DisplayName("'this' in List is preserved through inheritance") + void testGenericThisPreservedInInheritance() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointClass binaryExpr = langSpec.getJoinPoint("binaryExpr"); + + // childList is defined on node + Attribute childListOnNode = findAttribute(node.getAttributesSelf(), "childList"); + assertNotNull(childListOnNode); + + // binaryExpr extends expr extends node + Attribute childListOnBinaryExpr = findAttribute(binaryExpr.getAttributes(), "childList"); + assertNotNull(childListOnBinaryExpr); + + // Both should have List with ThisType inside + assertInstanceOf(ParameterizedType.class, childListOnNode.getType()); + assertInstanceOf(ParameterizedType.class, childListOnBinaryExpr.getType()); + + ParameterizedType nodeType = (ParameterizedType) childListOnNode.getType(); + ParameterizedType binaryType = (ParameterizedType) childListOnBinaryExpr.getType(); + + assertInstanceOf(ThisType.class, nodeType.getTypeArguments().get(0)); + assertInstanceOf(ThisType.class, binaryType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("Deep inheritance chain preserves 'this' type") + void testDeepInheritancePreservesThis() { + // loop extends stmt extends node + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + // Clone is defined on node, should be available on loop + Attribute cloneOnLoop = findAttribute(loop.getAttributes(), "clone"); + assertNotNull(cloneOnLoop); + assertInstanceOf(ThisType.class, cloneOnLoop.getType()); + + // childList is also from node + Attribute childListOnLoop = findAttribute(loop.getAttributes(), "childList"); + assertNotNull(childListOnLoop); + assertInstanceOf(ParameterizedType.class, childListOnLoop.getType()); + assertInstanceOf(ThisType.class, ((ParameterizedType) childListOnLoop.getType()).getTypeArguments().get(0)); + } + + @Test + @DisplayName("Global 'this' attributes are inherited by all join points") + void testGlobalThisInheritedByAll() { + JoinPointClass global = langSpec.getGlobal(); + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + // root is defined on global with type 'this' + Attribute rootOnGlobal = findAttribute(global.getAttributesSelf(), "root"); + assertNotNull(rootOnGlobal); + assertInstanceOf(ThisType.class, rootOnGlobal.getType()); + + // loop should inherit it + Attribute rootOnLoop = findAttribute(loop.getAttributes(), "root"); + assertNotNull(rootOnLoop); + assertInstanceOf(ThisType.class, rootOnLoop.getType()); + } + + @Test + @DisplayName("Actions with 'this' are correctly inherited") + void testActionsWithThisInherited() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + // copy is defined on node + Action copyOnNode = findAction(node.getActionsSelf(), "copy"); + assertNotNull(copyOnNode); + assertEquals("this", copyOnNode.getReturnType()); + + // loop should inherit it + Action copyOnLoop = findAction(loop.getActions(), "copy"); + assertNotNull(copyOnLoop); + assertEquals("this", copyOnLoop.getReturnType()); + } + } + + // ==================== TypeDef Tests ==================== + + @Nested + @DisplayName("'this' and Generics in TypeDef") + class TypeDefTests { + + @Test + @DisplayName("TypeDef with 'this' field exists") + void testTypeDefWithThisExists() { + assertTrue(langSpec.hasTypeDef("NodeInfo")); + TypeDef nodeInfo = langSpec.getTypeDefs().get("NodeInfo"); + assertNotNull(nodeInfo); + } + + @Test + @DisplayName("TypeDef field with 'this' type") + void testTypeDefFieldWithThis() { + TypeDef nodeInfo = langSpec.getTypeDefs().get("NodeInfo"); + Attribute nodeField = findAttribute(nodeInfo.getFields(), "node"); + + assertNotNull(nodeField); + assertInstanceOf(ThisType.class, nodeField.getType()); + assertEquals("this", nodeField.getType().toString()); + } + + @Test + @DisplayName("TypeDef field with List") + void testTypeDefFieldWithListThis() { + TypeDef nodeInfo = langSpec.getTypeDefs().get("NodeInfo"); + Attribute relatedNodes = findAttribute(nodeInfo.getFields(), "relatedNodes"); + + assertNotNull(relatedNodes); + assertInstanceOf(ParameterizedType.class, relatedNodes.getType()); + assertEquals("List", relatedNodes.getType().toString()); + } + + @Test + @DisplayName("TypeDef with nested generics containing 'this'") + void testTypeDefWithNestedGenerics() { + TypeDef treeStructure = langSpec.getTypeDefs().get("TreeStructure"); + assertNotNull(treeStructure); + + Attribute levels = findAttribute(treeStructure.getFields(), "levels"); + assertNotNull(levels); + assertInstanceOf(ParameterizedType.class, levels.getType()); + assertEquals("List>", levels.getType().toString()); + } + + @Test + @DisplayName("TypeDef with Map") + void testTypeDefWithMapThis() { + TypeDef treeStructure = langSpec.getTypeDefs().get("TreeStructure"); + Attribute nodeIndex = findAttribute(treeStructure.getFields(), "nodeIndex"); + + assertNotNull(nodeIndex); + assertInstanceOf(ParameterizedType.class, nodeIndex.getType()); + assertEquals("Map", nodeIndex.getType().toString()); + } + + @Test + @DisplayName("TypeDef without 'this' for comparison") + void testTypeDefWithoutThis() { + TypeDef simpleMetadata = langSpec.getTypeDefs().get("SimpleMetadata"); + assertNotNull(simpleMetadata); + + Attribute values = findAttribute(simpleMetadata.getFields(), "values"); + assertNotNull(values); + assertInstanceOf(ParameterizedType.class, values.getType()); + assertEquals("List", values.getType().toString()); + // Verify no ThisType in arguments + ParameterizedType valuesType = (ParameterizedType) values.getType(); + assertFalse(valuesType.getTypeArguments().stream().anyMatch(t -> t instanceof ThisType)); + } + + @Test + @DisplayName("TypeDef fields are correctly ordered") + void testTypeDefFieldOrder() { + TypeDef nodeInfo = langSpec.getTypeDefs().get("NodeInfo"); + List fields = nodeInfo.getFields(); + + // Fields should be sorted alphabetically + assertTrue(fields.size() >= 4); + + // Check metadata, name, node, relatedNodes are present + assertNotNull(findAttribute(fields, "metadata")); + assertNotNull(findAttribute(fields, "name")); + assertNotNull(findAttribute(fields, "node")); + assertNotNull(findAttribute(fields, "relatedNodes")); + } + } + + // ==================== AST/JSON Output Tests ==================== + + @Nested + @DisplayName("AST and JSON Output Verification") + class AstJsonTests { + + @Test + @DisplayName("NodeFactory creates valid RootNode from langspec") + void testNodeFactoryCreatesRootNode() { + RootNode rootNode = NodeFactory.toNode(langSpec); + assertNotNull(rootNode); + assertFalse(rootNode.getChildren().isEmpty()); + } + + @Test + @DisplayName("JoinPointNode is created correctly for 'this' attributes") + void testJoinPointNodeWithThisAttributes() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointNode jpNode = NodeFactory.toNode(node); + + assertNotNull(jpNode); + + // Find the attribute node for 'clone' + Optional cloneAttrNode = jpNode.getChildren().stream() + .filter(child -> child instanceof AttributeNode) + .map(child -> (AttributeNode) child) + .filter(attr -> "clone".equals(attr.getDeclaration().getName())) + .findFirst(); + + assertTrue(cloneAttrNode.isPresent()); + assertEquals("this", cloneAttrNode.get().getDeclaration().getType()); + } + + @Test + @DisplayName("JSON output contains 'this' string for ThisType") + void testJsonOutputContainsThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointNode jpNode = NodeFactory.toNode(node); + String json = jpNode.toJson(); + + // JSON should contain "type": "this" + assertTrue(json.contains("\"type\": \"this\""), + "JSON should contain '\"type\": \"this\"' for ThisType attributes"); + } + + @Test + @DisplayName("JSON output contains generic types with 'this'") + void testJsonOutputContainsGenericWithThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointNode jpNode = NodeFactory.toNode(node); + String json = jpNode.toJson(); + + // JSON should contain List + assertTrue(json.contains("List"), + "JSON should contain 'List' for generic attributes"); + } + + @Test + @DisplayName("JSON output preserves nested generics with 'this'") + void testJsonOutputPreservesNestedGenerics() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointNode jpNode = NodeFactory.toNode(node); + String json = jpNode.toJson(); + + // Should contain Map> + assertTrue(json.contains("Map>"), + "JSON should preserve nested generic types"); + } + + @Test + @DisplayName("JSON output for actions with 'this' return type") + void testJsonOutputActionWithThisReturn() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointNode jpNode = NodeFactory.toNode(node); + String json = jpNode.toJson(); + + // Actions section should contain return type "this" + assertTrue(json.contains("\"action\""), + "JSON should contain action nodes"); + + // The copy action returns 'this' + // We verify the structure is correct + assertFalse(json.isEmpty()); + } + + @Test + @DisplayName("JSON output for TypeDef with 'this' fields") + void testJsonOutputTypeDefWithThis() { + TypeDef nodeInfo = langSpec.getTypeDefs().get("NodeInfo"); + assertNotNull(nodeInfo); + + TypeDefNode tdNode = findTypeDefNode("NodeInfo"); + assertNotNull(tdNode); + + String json = tdNode.toJson(); + assertTrue(json.contains("\"type\": \"this\""), + "TypeDef JSON should contain 'this' type for fields"); + } + + @Test + @DisplayName("RootNode JSON is valid") + void testRootNodeJsonIsValid() { + RootNode rootNode = NodeFactory.toNode(langSpec); + String json = rootNode.toJson(); + + assertNotNull(json); + assertFalse(json.isEmpty()); + + // Basic JSON structure validation + assertTrue(json.contains("{")); + assertTrue(json.contains("}")); + assertTrue(json.contains("\"children\"")); + } + + @Test + @DisplayName("DeclarationNode preserves 'this' type string") + void testDeclarationNodePreservesThisType() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute clone = findAttribute(node.getAttributesSelf(), "clone"); + assertNotNull(clone); + + DeclarationNode declNode = new DeclarationNode(clone.getDeclaration()); + assertEquals("this", declNode.getType()); + + String json = declNode.toJson(); + assertTrue(json.contains("\"this\"")); + } + + @Test + @DisplayName("AttributeNode JSON includes parameters") + void testAttributeNodeJsonIncludesParams() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute ancestorOfType = findAttribute(node.getAttributesSelf(), "ancestorOfType"); + assertNotNull(ancestorOfType); + + AttributeNode attrNode = NodeFactory.toNode(ancestorOfType); + String json = attrNode.toJson(); + + assertTrue(json.contains("\"type\": \"this\"")); + assertTrue(json.contains("\"typeName\"")); + } + + @Test + @DisplayName("ActionNode JSON for action with 'this' parameter") + void testActionNodeJsonWithThisParam() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action insertBefore = findAction(node.getActionsSelf(), "insertBefore"); + assertNotNull(insertBefore); + + ActionNode actionNode = NodeFactory.toNode(insertBefore); + String json = actionNode.toJson(); + + // Should contain parameter with type 'this' + assertTrue(json.contains("\"this\""), + "Action JSON should contain 'this' for parameter type"); + } + + private TypeDefNode findTypeDefNode(String name) { + RootNode rootNode = NodeFactory.toNode(langSpec); + return rootNode.getChildren().stream() + .filter(child -> child instanceof TypeDefNode) + .map(child -> (TypeDefNode) child) + .filter(td -> name.equals(td.getName())) + .findFirst() + .orElse(null); + } + } + + // ==================== Edge Cases and Error Handling ==================== + + @Nested + @DisplayName("Edge Cases and Special Scenarios") + class EdgeCasesTests { + + @Test + @DisplayName("ThisType singleton is consistent across all usages") + void testThisTypeSingleton() { + JoinPointClass node = langSpec.getJoinPoint("node"); + JoinPointClass global = langSpec.getGlobal(); + + Attribute clone = findAttribute(node.getAttributesSelf(), "clone"); + Attribute parent = findAttribute(node.getAttributesSelf(), "parent"); + Attribute rootAttr = findAttribute(global.getAttributesSelf(), "root"); + + // All should reference the same singleton instance + assertSame(clone.getType(), parent.getType()); + assertSame(clone.getType(), rootAttr.getType()); + assertSame(ThisType.getInstance(), clone.getType()); + } + + @Test + @DisplayName("'this' in different join points refers to same ThisType instance") + void testThisTypeSameAcrossJoinPoints() { + JoinPointClass expr = langSpec.getJoinPoint("expr"); + JoinPointClass stmt = langSpec.getJoinPoint("stmt"); + + Attribute simplify = findAttribute(expr.getAttributesSelf(), "simplify"); + Attribute next = findAttribute(stmt.getAttributesSelf(), "next"); + + assertNotNull(simplify); + assertNotNull(next); + + // Both should be the same ThisType instance + assertSame(simplify.getType(), next.getType()); + } + + @Test + @DisplayName("ThisType presence can be verified via instanceof checks") + void testThisTypePresenceInNestedStructures() { + JoinPointClass node = langSpec.getJoinPoint("node"); + + // Simple generic without 'this' + Attribute tags = findAttribute(node.getAttributesSelf(), "tags"); + ParameterizedType tagsType = (ParameterizedType) tags.getType(); + assertFalse(tagsType.getTypeArguments().stream().anyMatch(t -> t instanceof ThisType)); + + // Generic with 'this' + Attribute childList = findAttribute(node.getAttributesSelf(), "childList"); + ParameterizedType childListType = (ParameterizedType) childList.getType(); + assertInstanceOf(ThisType.class, childListType.getTypeArguments().get(0)); + + // Nested generic with 'this' + Attribute childGroups = findAttribute(node.getAttributesSelf(), "childGroups"); + ParameterizedType childGroupsType = (ParameterizedType) childGroups.getType(); + ParameterizedType innerListType = (ParameterizedType) childGroupsType.getTypeArguments().get(0); + assertInstanceOf(ThisType.class, innerListType.getTypeArguments().get(0)); + } + + @Test + @DisplayName("toString() consistency for various 'this' type configurations") + void testToStringConsistency() { + assertEquals("this", ThisType.getInstance().toString()); + assertEquals("this", ThisType.getInstance().type()); + + JoinPointClass node = langSpec.getJoinPoint("node"); + + Attribute children = findAttribute(node.getAttributesSelf(), "children"); + assertEquals("this[]", children.getType().toString()); + + Attribute childList = findAttribute(node.getAttributesSelf(), "childList"); + assertEquals("List", childList.getType().toString()); + + Attribute namedChildren = findAttribute(node.getAttributesSelf(), "namedChildren"); + assertEquals("Map", namedChildren.getType().toString()); + } + + @Test + @DisplayName("Attribute default values work with join points having 'this' attributes") + void testDefaultAttributeWithThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + assertTrue(node.getDefaultAttribute().isPresent()); + assertEquals("id", node.getDefaultAttribute().get()); + } + + @Test + @DisplayName("Tooltips are preserved for 'this' type attributes") + void testTooltipsPreservedForThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Attribute clone = findAttribute(node.getAttributesSelf(), "clone"); + + assertNotNull(clone); + assertTrue(clone.getToolTip().isPresent()); + assertEquals("Creates a deep clone of this node", clone.getToolTip().get()); + } + + @Test + @DisplayName("Actions with 'this' preserve tooltips") + void testActionTooltipsWithThis() { + JoinPointClass node = langSpec.getJoinPoint("node"); + Action copy = findAction(node.getActionsSelf(), "copy"); + + assertNotNull(copy); + assertTrue(copy.getToolTip().isPresent()); + assertEquals("Creates a copy of this node", copy.getToolTip().get()); + } + + @Test + @DisplayName("All expected join point-specific attributes are present") + void testJoinPointSpecificAttributesPresent() { + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + // Own attributes + assertNotNull(findAttribute(loop.getAttributesSelf(), "kind")); + assertNotNull(findAttribute(loop.getAttributesSelf(), "body")); + assertNotNull(findAttribute(loop.getAttributesSelf(), "nestedLoops")); + + // Inherited from node + assertNotNull(findAttribute(loop.getAttributes(), "clone")); + assertNotNull(findAttribute(loop.getAttributes(), "childList")); + } + } + + // ==================== Specific Join Point Tests ==================== + + @Nested + @DisplayName("Join Point Specific Feature Tests") + class JoinPointSpecificTests { + + @Test + @DisplayName("Expression join point 'this' attributes") + void testExpressionThisAttributes() { + JoinPointClass expr = langSpec.getJoinPoint("expr"); + + Attribute simplify = findAttribute(expr.getAttributesSelf(), "simplify"); + Attribute normalize = findAttribute(expr.getAttributesSelf(), "normalize"); + Attribute subExpressions = findAttribute(expr.getAttributesSelf(), "subExpressions"); + + assertNotNull(simplify); + assertNotNull(normalize); + assertNotNull(subExpressions); + + assertInstanceOf(ThisType.class, simplify.getType()); + assertInstanceOf(ThisType.class, normalize.getType()); + assertInstanceOf(ParameterizedType.class, subExpressions.getType()); + assertEquals("List", subExpressions.getType().toString()); + } + + @Test + @DisplayName("BinaryExpr join point 'this' attributes") + void testBinaryExprThisAttributes() { + JoinPointClass binaryExpr = langSpec.getJoinPoint("binaryExpr"); + + Attribute left = findAttribute(binaryExpr.getAttributesSelf(), "left"); + Attribute right = findAttribute(binaryExpr.getAttributesSelf(), "right"); + Attribute operands = findAttribute(binaryExpr.getAttributesSelf(), "operands"); + + assertNotNull(left); + assertNotNull(right); + assertNotNull(operands); + + assertInstanceOf(ThisType.class, left.getType()); + assertInstanceOf(ThisType.class, right.getType()); + assertEquals("List", operands.getType().toString()); + } + + @Test + @DisplayName("Loop join point actions with 'this'") + void testLoopActionsWithThis() { + JoinPointClass loop = langSpec.getJoinPoint("loop"); + + Action unroll = findAction(loop.getActionsSelf(), "unroll"); + Action tile = findAction(loop.getActionsSelf(), "tile"); + Action interchange = findAction(loop.getActionsSelf(), "interchange"); + Action fuse = findAction(loop.getActionsSelf(), "fuse"); + + assertNotNull(unroll); + assertNotNull(tile); + assertNotNull(interchange); + assertNotNull(fuse); + + assertEquals("List", unroll.getReturnType()); + assertEquals("this", tile.getReturnType()); + assertEquals("this", interchange.getParameters().get(0).getType()); + assertEquals("this", fuse.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Statement join point 'this' navigation attributes") + void testStmtNavigationAttributes() { + JoinPointClass stmt = langSpec.getJoinPoint("stmt"); + + Attribute next = findAttribute(stmt.getAttributesSelf(), "next"); + Attribute prev = findAttribute(stmt.getAttributesSelf(), "prev"); + Attribute following = findAttribute(stmt.getAttributesSelf(), "following"); + + assertNotNull(next); + assertNotNull(prev); + assertNotNull(following); + + assertInstanceOf(ThisType.class, next.getType()); + assertInstanceOf(ThisType.class, prev.getType()); + assertEquals("List", following.getType().toString()); + } + } + + // ==================== Enum and Object Type Tests ==================== + + @Nested + @DisplayName("Enum and Object Types Coexistence") + class EnumObjectTests { + + @Test + @DisplayName("Enum definition is parsed correctly") + void testEnumDefinition() { + assertTrue(langSpec.hasEnumDef("NodeKind")); + } + + @Test + @DisplayName("Object type is parsed correctly") + void testObjectType() { + assertTrue(langSpec.hasTypeDef("AstContext")); + } + + @Test + @DisplayName("TypeDefs, EnumDefs, and 'this' types coexist") + void testTypesCoexist() { + // TypeDefs with 'this' + assertTrue(langSpec.hasTypeDef("NodeInfo")); + assertTrue(langSpec.hasTypeDef("TreeStructure")); + + // EnumDefs + assertTrue(langSpec.hasEnumDef("NodeKind")); + + // Object types + assertTrue(langSpec.hasTypeDef("AstContext")); + + // All join points with 'this' attributes work + JoinPointClass node = langSpec.getJoinPoint("node"); + assertNotNull(findAttribute(node.getAttributesSelf(), "clone")); + } + } + + // ==================== Helper Methods ==================== + + private static Attribute findAttribute(List attributes, String name) { + return attributes.stream() + .filter(a -> name.equals(a.getName())) + .findFirst() + .orElse(null); + } + + private static Action findAction(List actions, String name) { + return actions.stream() + .filter(a -> name.equals(a.getName())) + .findFirst() + .orElse(null); + } +} From 6e454145b3c73ee91955382a54c313aee7b8c19b Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 31 Jan 2026 04:40:52 +0000 Subject: [PATCH 02/14] Add wildcard generics support and comprehensive tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add WildcardType IType implementation to represent ?, ? extends T, and ? super T, including factory methods (unbounded(), extendsType(), superType()), singleton for unbounded, proper toString()/type(), equals/hashCode and validation. - Parse wildcard type arguments in LanguageSpecification: - Detect wildcard tokens in getType() and route to parseWildcardType(...). - Parse ?, ? extends X, ? super X and build corresponding WildcardType instances. - Preserve nested generics and array handling (e.g., List, ? extends this[]). - Add extensive XML fixtures for wildcard scenarios: - artifacts.xml - actionModel.xml - joinPointModel.xml - Fixtures include nested wildcards, wildcard arrays, multiple wildcards, this-bound wildcards, and attributes with parameters using wildcard types. - Add integration tests: - WildcardTypeXmlIntegrationTest.java — 39 tests covering parsing of wildcard generics, nested cases, arrays, actions, parameters, and inheritance with this. - Add unit tests: - WildcardTypeTest.java — comprehensive unit tests for WildcardType. - Small supporting changes: - Update LanguageSpecification.java to add wildcard handling and parsing helper. - Add attribute-parameter tests covering parameters whose types are wildcards and compound generics (e.g., Map, List). - Minor test resource and test class additions/adjustments to ensure coverage and validation. --- .../dsl/LanguageSpecification.java | 36 + .../specification/dsl/types/WildcardType.java | 204 +++++ .../lara/langspec/wildcards/actionModel.xml | 66 ++ .../lara/langspec/wildcards/artifacts.xml | 57 ++ .../langspec/wildcards/joinPointModel.xml | 5 + .../dsl/types/WildcardTypeTest.java | 543 ++++++++++++ .../langspec/ThisTypeXmlIntegrationTest.java | 18 +- .../WildcardTypeXmlIntegrationTest.java | 828 ++++++++++++++++++ 8 files changed, 1751 insertions(+), 6 deletions(-) create mode 100644 LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/actionModel.xml create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/artifacts.xml create mode 100644 LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/joinPointModel.xml create mode 100644 LanguageSpecification/test/org/lara/language/specification/dsl/types/WildcardTypeTest.java create mode 100644 LanguageSpecification/test/pt/up/fe/specs/lara/langspec/WildcardTypeXmlIntegrationTest.java diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java b/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java index 979272418..8b2337da9 100644 --- a/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/LanguageSpecification.java @@ -256,6 +256,11 @@ public IType getType(String type) { return ThisType.getInstance(); } + // Handle wildcard types: ?, ? extends X, ? super X + if (type.startsWith(WildcardType.WILDCARD_SYMBOL)) { + return parseWildcardType(type); + } + // Handle generic/parameterized types (e.g., List, Map) if (type.contains("<") && type.contains(">")) { return parseParameterizedType(type); @@ -362,6 +367,37 @@ private List parseTypeArguments(String argsString) { return arguments; } + /** + * Parses a wildcard type string (e.g., "?", "? extends String", "? super this"). + * + * @param type the wildcard type string starting with "?" + * @return a WildcardType representing the parsed wildcard + */ + private IType parseWildcardType(String type) { + String trimmed = type.trim(); + + // Unbounded wildcard: just "?" + if (trimmed.equals(WildcardType.WILDCARD_SYMBOL)) { + return WildcardType.unbounded(); + } + + // Upper bounded: "? extends X" + if (trimmed.startsWith("? extends ")) { + String boundTypeName = trimmed.substring("? extends ".length()).trim(); + IType boundType = getType(boundTypeName); + return WildcardType.extendsType(boundType); + } + + // Lower bounded: "? super X" + if (trimmed.startsWith("? super ")) { + String boundTypeName = trimmed.substring("? super ".length()).trim(); + IType boundType = getType(boundTypeName); + return WildcardType.superType(boundType); + } + + throw new RuntimeException("Invalid wildcard type format: " + type); + } + public JoinPointClass getRoot() { return root; } diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java b/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java new file mode 100644 index 000000000..7e3fc22a5 --- /dev/null +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java @@ -0,0 +1,204 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +import java.util.Objects; + +/** + * Represents a wildcard type used in generic type arguments. + * + *

Supports three kinds of wildcards: + *

    + *
  • {@link Kind#UNBOUNDED} - {@code ?} - matches any type
  • + *
  • {@link Kind#EXTENDS} - {@code ? extends T} - upper bounded wildcard
  • + *
  • {@link Kind#SUPER} - {@code ? super T} - lower bounded wildcard
  • + *
+ *

+ * + *

Examples: + *

    + *
  • {@code List} - list of unknown type
  • + *
  • {@code List} - list of Number or subtype
  • + *
  • {@code List} - list of Integer or supertype
  • + *
  • {@code Map} - wildcard with ThisType bound
  • + *
+ *

+ */ +public class WildcardType implements IType { + + /** The wildcard symbol. */ + public static final String WILDCARD_SYMBOL = "?"; + + /** + * Represents the kind of wildcard bound. + */ + public enum Kind { + /** Unbounded wildcard: {@code ?} */ + UNBOUNDED(""), + /** Upper bounded wildcard: {@code ? extends T} */ + EXTENDS("extends"), + /** Lower bounded wildcard: {@code ? super T} */ + SUPER("super"); + + private final String keyword; + + Kind(String keyword) { + this.keyword = keyword; + } + + /** + * Returns the keyword used in the type syntax (empty for unbounded). + * + * @return the keyword + */ + public String getKeyword() { + return keyword; + } + } + + /** Singleton instance for unbounded wildcard. */ + private static final WildcardType UNBOUNDED_INSTANCE = new WildcardType(Kind.UNBOUNDED, null); + + private final Kind kind; + private final IType bound; + + /** + * Creates a wildcard type with the specified kind and bound. + * + * @param kind the wildcard kind + * @param bound the bound type (must be null for UNBOUNDED, non-null for EXTENDS/SUPER) + * @throws NullPointerException if kind is null + * @throws IllegalArgumentException if bound is inconsistent with the kind + */ + public WildcardType(Kind kind, IType bound) { + Objects.requireNonNull(kind, "Wildcard kind cannot be null"); + + if (kind == Kind.UNBOUNDED && bound != null) { + throw new IllegalArgumentException("Unbounded wildcard cannot have a bound type"); + } + if (kind != Kind.UNBOUNDED && bound == null) { + throw new IllegalArgumentException("Bounded wildcard (" + kind + ") requires a bound type"); + } + + this.kind = kind; + this.bound = bound; + } + + /** + * Returns an unbounded wildcard ({@code ?}). + * + * @return the unbounded wildcard singleton + */ + public static WildcardType unbounded() { + return UNBOUNDED_INSTANCE; + } + + /** + * Creates an upper bounded wildcard ({@code ? extends T}). + * + * @param bound the upper bound type + * @return the wildcard type + * @throws NullPointerException if bound is null + */ + public static WildcardType extendsType(IType bound) { + Objects.requireNonNull(bound, "Bound type cannot be null for extends wildcard"); + return new WildcardType(Kind.EXTENDS, bound); + } + + /** + * Creates a lower bounded wildcard ({@code ? super T}). + * + * @param bound the lower bound type + * @return the wildcard type + * @throws NullPointerException if bound is null + */ + public static WildcardType superType(IType bound) { + Objects.requireNonNull(bound, "Bound type cannot be null for super wildcard"); + return new WildcardType(Kind.SUPER, bound); + } + + /** + * Returns the wildcard kind. + * + * @return the kind (UNBOUNDED, EXTENDS, or SUPER) + */ + public Kind getKind() { + return kind; + } + + /** + * Returns the bound type, if any. + * + * @return the bound type, or null for unbounded wildcards + */ + public IType getBound() { + return bound; + } + + /** + * Checks if this is an unbounded wildcard. + * + * @return true if unbounded + */ + public boolean isUnbounded() { + return kind == Kind.UNBOUNDED; + } + + /** + * Checks if this is an upper bounded wildcard (extends). + * + * @return true if upper bounded + */ + public boolean isUpperBounded() { + return kind == Kind.EXTENDS; + } + + /** + * Checks if this is a lower bounded wildcard (super). + * + * @return true if lower bounded + */ + public boolean isLowerBounded() { + return kind == Kind.SUPER; + } + + @Override + public String type() { + if (kind == Kind.UNBOUNDED) { + return WILDCARD_SYMBOL; + } + return WILDCARD_SYMBOL + " " + kind.getKeyword() + " " + bound.toString(); + } + + @Override + public String toString() { + return type(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof WildcardType other)) { + return false; + } + return kind == other.kind && Objects.equals(bound, other.bound); + } + + @Override + public int hashCode() { + return Objects.hash(kind, bound); + } +} diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/actionModel.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/actionModel.xml new file mode 100644 index 000000000..09c611855 --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/actionModel.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/artifacts.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/artifacts.xml new file mode 100644 index 000000000..a9b85e32c --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/artifacts.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/joinPointModel.xml b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/joinPointModel.xml new file mode 100644 index 000000000..4272b9da3 --- /dev/null +++ b/LanguageSpecification/test-resources/pt/up/fe/specs/lara/langspec/wildcards/joinPointModel.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/LanguageSpecification/test/org/lara/language/specification/dsl/types/WildcardTypeTest.java b/LanguageSpecification/test/org/lara/language/specification/dsl/types/WildcardTypeTest.java new file mode 100644 index 000000000..d3db6b113 --- /dev/null +++ b/LanguageSpecification/test/org/lara/language/specification/dsl/types/WildcardTypeTest.java @@ -0,0 +1,543 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.lara.language.specification.dsl.types; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link WildcardType}. + * + *

Tests cover factory methods, kind checking, string representation, + * bounds access, equality/hashCode, validation, and integration with + * other IType implementations.

+ */ +@DisplayName("WildcardType Tests") +class WildcardTypeTest { + + @Nested + @DisplayName("Factory Method Tests") + class FactoryMethodTests { + + @Test + @DisplayName("unbounded() should create unbounded wildcard") + void testUnboundedFactory() { + WildcardType wildcard = WildcardType.unbounded(); + + assertNotNull(wildcard); + assertEquals(WildcardType.Kind.UNBOUNDED, wildcard.getKind()); + assertNull(wildcard.getBound()); + } + + @Test + @DisplayName("extendsType() should create upper bounded wildcard") + void testExtendsTypeFactory() { + IType bound = PrimitiveClasses.STRING; + WildcardType wildcard = WildcardType.extendsType(bound); + + assertNotNull(wildcard); + assertEquals(WildcardType.Kind.EXTENDS, wildcard.getKind()); + assertEquals(bound, wildcard.getBound()); + } + + @Test + @DisplayName("superType() should create lower bounded wildcard") + void testSuperTypeFactory() { + IType bound = PrimitiveClasses.INTEGER; + WildcardType wildcard = WildcardType.superType(bound); + + assertNotNull(wildcard); + assertEquals(WildcardType.Kind.SUPER, wildcard.getKind()); + assertEquals(bound, wildcard.getBound()); + } + + @Test + @DisplayName("extendsType() should reject null bound") + void testExtendsTypeRejectsNull() { + assertThrows(NullPointerException.class, () -> WildcardType.extendsType(null)); + } + + @Test + @DisplayName("superType() should reject null bound") + void testSuperTypeRejectsNull() { + assertThrows(NullPointerException.class, () -> WildcardType.superType(null)); + } + } + + @Nested + @DisplayName("Kind Checking Tests") + class KindCheckingTests { + + @Test + @DisplayName("isUnbounded() should return true for unbounded wildcard") + void testIsUnboundedTrue() { + WildcardType wildcard = WildcardType.unbounded(); + + assertTrue(wildcard.isUnbounded()); + assertFalse(wildcard.isUpperBounded()); + assertFalse(wildcard.isLowerBounded()); + } + + @Test + @DisplayName("isUpperBounded() should return true for extends wildcard") + void testIsUpperBoundedTrue() { + WildcardType wildcard = WildcardType.extendsType(PrimitiveClasses.STRING); + + assertFalse(wildcard.isUnbounded()); + assertTrue(wildcard.isUpperBounded()); + assertFalse(wildcard.isLowerBounded()); + } + + @Test + @DisplayName("isLowerBounded() should return true for super wildcard") + void testIsLowerBoundedTrue() { + WildcardType wildcard = WildcardType.superType(PrimitiveClasses.STRING); + + assertFalse(wildcard.isUnbounded()); + assertFalse(wildcard.isUpperBounded()); + assertTrue(wildcard.isLowerBounded()); + } + } + + @Nested + @DisplayName("String Representation Tests") + class StringRepresentationTests { + + @Test + @DisplayName("Unbounded wildcard type() should return '?'") + void testUnboundedType() { + WildcardType wildcard = WildcardType.unbounded(); + + assertEquals("?", wildcard.type()); + } + + @Test + @DisplayName("Upper bounded wildcard type() should return '? extends T'") + void testUpperBoundedType() { + WildcardType wildcard = WildcardType.extendsType(PrimitiveClasses.STRING); + + assertEquals("? extends String", wildcard.type()); + } + + @Test + @DisplayName("Lower bounded wildcard type() should return '? super T'") + void testLowerBoundedType() { + WildcardType wildcard = WildcardType.superType(PrimitiveClasses.INTEGER); + + assertEquals("? super Integer", wildcard.type()); + } + + @Test + @DisplayName("toString() should match type() for unbounded") + void testToStringMatchesTypeUnbounded() { + WildcardType wildcard = WildcardType.unbounded(); + + assertEquals(wildcard.type(), wildcard.toString()); + assertEquals("?", wildcard.toString()); + } + + @Test + @DisplayName("toString() should match type() for upper bounded") + void testToStringMatchesTypeUpperBounded() { + WildcardType wildcard = WildcardType.extendsType(PrimitiveClasses.DOUBLE); + + assertEquals(wildcard.type(), wildcard.toString()); + assertEquals("? extends Double", wildcard.toString()); + } + + @Test + @DisplayName("toString() should match type() for lower bounded") + void testToStringMatchesTypeLowerBounded() { + WildcardType wildcard = WildcardType.superType(PrimitiveClasses.LONG); + + assertEquals(wildcard.type(), wildcard.toString()); + assertEquals("? super Long", wildcard.toString()); + } + } + + @Nested + @DisplayName("Bounds Access Tests") + class BoundsAccessTests { + + @Test + @DisplayName("getBound() should return null for unbounded wildcard") + void testGetBoundUnbounded() { + WildcardType wildcard = WildcardType.unbounded(); + + assertNull(wildcard.getBound()); + } + + @Test + @DisplayName("getBound() should return bound type for extends wildcard") + void testGetBoundExtends() { + IType bound = PrimitiveClasses.STRING; + WildcardType wildcard = WildcardType.extendsType(bound); + + assertSame(bound, wildcard.getBound()); + } + + @Test + @DisplayName("getBound() should return bound type for super wildcard") + void testGetBoundSuper() { + IType bound = PrimitiveClasses.INTEGER; + WildcardType wildcard = WildcardType.superType(bound); + + assertSame(bound, wildcard.getBound()); + } + + @Test + @DisplayName("getKind() should return correct kind for each type") + void testGetKindAllTypes() { + assertEquals(WildcardType.Kind.UNBOUNDED, WildcardType.unbounded().getKind()); + assertEquals(WildcardType.Kind.EXTENDS, WildcardType.extendsType(PrimitiveClasses.STRING).getKind()); + assertEquals(WildcardType.Kind.SUPER, WildcardType.superType(PrimitiveClasses.STRING).getKind()); + } + } + + @Nested + @DisplayName("Equality and HashCode Tests") + class EqualityHashCodeTests { + + @Test + @DisplayName("Unbounded wildcards should be equal") + void testUnboundedEquality() { + WildcardType w1 = WildcardType.unbounded(); + WildcardType w2 = WildcardType.unbounded(); + + assertEquals(w1, w2); + assertEquals(w1.hashCode(), w2.hashCode()); + } + + @Test + @DisplayName("Same extends wildcards should be equal") + void testExtendsEquality() { + WildcardType w1 = WildcardType.extendsType(PrimitiveClasses.STRING); + WildcardType w2 = WildcardType.extendsType(PrimitiveClasses.STRING); + + assertEquals(w1, w2); + assertEquals(w1.hashCode(), w2.hashCode()); + } + + @Test + @DisplayName("Same super wildcards should be equal") + void testSuperEquality() { + WildcardType w1 = WildcardType.superType(PrimitiveClasses.INTEGER); + WildcardType w2 = WildcardType.superType(PrimitiveClasses.INTEGER); + + assertEquals(w1, w2); + assertEquals(w1.hashCode(), w2.hashCode()); + } + + @Test + @DisplayName("Different kind wildcards should not be equal") + void testDifferentKindNotEqual() { + WildcardType unbounded = WildcardType.unbounded(); + WildcardType extendsW = WildcardType.extendsType(PrimitiveClasses.STRING); + WildcardType superW = WildcardType.superType(PrimitiveClasses.STRING); + + assertNotEquals(unbounded, extendsW); + assertNotEquals(unbounded, superW); + assertNotEquals(extendsW, superW); + } + + @Test + @DisplayName("Different bound types should not be equal") + void testDifferentBoundsNotEqual() { + WildcardType w1 = WildcardType.extendsType(PrimitiveClasses.STRING); + WildcardType w2 = WildcardType.extendsType(PrimitiveClasses.INTEGER); + + assertNotEquals(w1, w2); + } + + @Test + @DisplayName("Wildcard should equal itself") + void testEqualsItself() { + WildcardType wildcard = WildcardType.extendsType(PrimitiveClasses.STRING); + + assertEquals(wildcard, wildcard); + } + + @Test + @DisplayName("Wildcard should not equal null") + void testNotEqualsNull() { + WildcardType wildcard = WildcardType.unbounded(); + + assertNotEquals(null, wildcard); + } + + @Test + @DisplayName("Wildcard should not equal other types") + void testNotEqualsOtherTypes() { + WildcardType wildcard = WildcardType.unbounded(); + + assertNotEquals(wildcard, "?"); + assertNotEquals(wildcard, new GenericType("?", false)); + assertNotEquals(wildcard, PrimitiveClasses.STRING); + } + + @Test + @DisplayName("HashCode should be consistent") + void testHashCodeConsistency() { + WildcardType wildcard = WildcardType.extendsType(PrimitiveClasses.DOUBLE); + + int hash1 = wildcard.hashCode(); + int hash2 = wildcard.hashCode(); + + assertEquals(hash1, hash2); + } + } + + @Nested + @DisplayName("Validation Tests") + class ValidationTests { + + @Test + @DisplayName("Constructor should reject null kind") + void testNullKindRejected() { + assertThrows(NullPointerException.class, () -> new WildcardType(null, null)); + } + + @Test + @DisplayName("Constructor should reject non-null bound for UNBOUNDED") + void testUnboundedWithBoundRejected() { + assertThrows(IllegalArgumentException.class, () -> + new WildcardType(WildcardType.Kind.UNBOUNDED, PrimitiveClasses.STRING)); + } + + @Test + @DisplayName("Constructor should reject null bound for EXTENDS") + void testExtendsWithoutBoundRejected() { + assertThrows(IllegalArgumentException.class, () -> + new WildcardType(WildcardType.Kind.EXTENDS, null)); + } + + @Test + @DisplayName("Constructor should reject null bound for SUPER") + void testSuperWithoutBoundRejected() { + assertThrows(IllegalArgumentException.class, () -> + new WildcardType(WildcardType.Kind.SUPER, null)); + } + } + + @Nested + @DisplayName("Singleton Behavior Tests") + class SingletonBehaviorTests { + + @Test + @DisplayName("unbounded() should return same instance (singleton)") + void testUnboundedSingleton() { + WildcardType w1 = WildcardType.unbounded(); + WildcardType w2 = WildcardType.unbounded(); + + assertSame(w1, w2); + } + + @Test + @DisplayName("Bounded wildcards should not be singletons") + void testBoundedNotSingleton() { + WildcardType e1 = WildcardType.extendsType(PrimitiveClasses.STRING); + WildcardType e2 = WildcardType.extendsType(PrimitiveClasses.STRING); + + // They are equal but not same instance + assertEquals(e1, e2); + assertNotSame(e1, e2); + } + } + + @Nested + @DisplayName("ThisType as Bound Tests") + class ThisTypeAsBoundTests { + + @Test + @DisplayName("Should support ThisType as extends bound") + void testExtendsThis() { + WildcardType wildcard = WildcardType.extendsType(ThisType.getInstance()); + + assertEquals(WildcardType.Kind.EXTENDS, wildcard.getKind()); + assertSame(ThisType.getInstance(), wildcard.getBound()); + assertEquals("? extends this", wildcard.toString()); + } + + @Test + @DisplayName("Should support ThisType as super bound") + void testSuperThis() { + WildcardType wildcard = WildcardType.superType(ThisType.getInstance()); + + assertEquals(WildcardType.Kind.SUPER, wildcard.getKind()); + assertSame(ThisType.getInstance(), wildcard.getBound()); + assertEquals("? super this", wildcard.toString()); + } + + @Test + @DisplayName("Wildcards with ThisType should have correct equality") + void testThisTypeEquality() { + WildcardType w1 = WildcardType.extendsType(ThisType.getInstance()); + WildcardType w2 = WildcardType.extendsType(ThisType.getInstance()); + + assertEquals(w1, w2); + assertEquals(w1.hashCode(), w2.hashCode()); + } + } + + @Nested + @DisplayName("IType Implementations as Bounds Tests") + class ITypeImplementationsAsBoundsTests { + + @Test + @DisplayName("Should support GenericType as bound") + void testGenericTypeBound() { + GenericType bound = new GenericType("List", false); + WildcardType wildcard = WildcardType.extendsType(bound); + + assertEquals(bound, wildcard.getBound()); + assertEquals("? extends List", wildcard.toString()); + } + + @Test + @DisplayName("Should support ArrayType as bound") + void testArrayTypeBound() { + ArrayType bound = new ArrayType(PrimitiveClasses.STRING, 1); + WildcardType wildcard = WildcardType.superType(bound); + + assertEquals(bound, wildcard.getBound()); + assertEquals("? super String[]", wildcard.toString()); + } + + @Test + @DisplayName("Should support ParameterizedType as bound") + void testParameterizedTypeBound() { + ParameterizedType bound = ParameterizedType.of( + new GenericType("List", false), PrimitiveClasses.STRING); + WildcardType wildcard = WildcardType.extendsType(bound); + + assertEquals(bound, wildcard.getBound()); + assertEquals("? extends List", wildcard.toString()); + } + + @Test + @DisplayName("Should support nested wildcards conceptually (wildcard as bound)") + void testNestedWildcardAsBound() { + // While unusual, this tests that the type system handles any IType + WildcardType innerWildcard = WildcardType.unbounded(); + WildcardType outerWildcard = WildcardType.extendsType(innerWildcard); + + assertEquals(innerWildcard, outerWildcard.getBound()); + assertEquals("? extends ?", outerWildcard.toString()); + } + + @Test + @DisplayName("Should support Primitive as bound") + void testPrimitiveBound() { + // Primitives implementing IType + WildcardType wildcard = WildcardType.extendsType(Primitive.INT); + + assertEquals(Primitive.INT, wildcard.getBound()); + assertEquals("? extends int", wildcard.toString()); + } + } + + @Nested + @DisplayName("IType Contract Tests") + class ITypeContractTests { + + @Test + @DisplayName("Should implement IType interface") + void testImplementsIType() { + WildcardType wildcard = WildcardType.unbounded(); + + assertInstanceOf(IType.class, wildcard); + } + + @Test + @DisplayName("Should not be an array type") + void testIsArrayFalse() { + WildcardType unbounded = WildcardType.unbounded(); + WildcardType extends_ = WildcardType.extendsType(PrimitiveClasses.STRING); + WildcardType super_ = WildcardType.superType(PrimitiveClasses.STRING); + + assertFalse(unbounded.isArray()); + assertFalse(extends_.isArray()); + assertFalse(super_.isArray()); + } + + @Test + @DisplayName("Should be usable as IType") + void testUsableAsIType() { + IType type = WildcardType.extendsType(PrimitiveClasses.STRING); + + assertEquals("? extends String", type.type()); + assertEquals("? extends String", type.toString()); + assertFalse(type.isArray()); + } + } + + @Nested + @DisplayName("Kind Enum Tests") + class KindEnumTests { + + @Test + @DisplayName("UNBOUNDED kind should have empty keyword") + void testUnboundedKeyword() { + assertEquals("", WildcardType.Kind.UNBOUNDED.getKeyword()); + } + + @Test + @DisplayName("EXTENDS kind should have 'extends' keyword") + void testExtendsKeyword() { + assertEquals("extends", WildcardType.Kind.EXTENDS.getKeyword()); + } + + @Test + @DisplayName("SUPER kind should have 'super' keyword") + void testSuperKeyword() { + assertEquals("super", WildcardType.Kind.SUPER.getKeyword()); + } + + @Test + @DisplayName("Kind enum should have exactly 3 values") + void testKindEnumValues() { + WildcardType.Kind[] values = WildcardType.Kind.values(); + + assertEquals(3, values.length); + } + + @Test + @DisplayName("Kind valueOf should work correctly") + void testKindValueOf() { + assertEquals(WildcardType.Kind.UNBOUNDED, WildcardType.Kind.valueOf("UNBOUNDED")); + assertEquals(WildcardType.Kind.EXTENDS, WildcardType.Kind.valueOf("EXTENDS")); + assertEquals(WildcardType.Kind.SUPER, WildcardType.Kind.valueOf("SUPER")); + } + } + + @Nested + @DisplayName("Constant Tests") + class ConstantTests { + + @Test + @DisplayName("WILDCARD_SYMBOL should be '?'") + void testWildcardSymbol() { + assertEquals("?", WildcardType.WILDCARD_SYMBOL); + } + + @Test + @DisplayName("Unbounded type() should use WILDCARD_SYMBOL") + void testUnboundedUsesSymbol() { + assertEquals(WildcardType.WILDCARD_SYMBOL, WildcardType.unbounded().type()); + } + } +} diff --git a/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java index ff319447c..858a5a85d 100644 --- a/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java +++ b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/ThisTypeXmlIntegrationTest.java @@ -13,6 +13,17 @@ package pt.up.fe.specs.lara.langspec; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -22,7 +33,6 @@ import org.lara.language.specification.ast.AttributeNode; import org.lara.language.specification.ast.DeclarationNode; import org.lara.language.specification.ast.JoinPointNode; -import org.lara.language.specification.ast.LangSpecNode; import org.lara.language.specification.ast.NodeFactory; import org.lara.language.specification.ast.RootNode; import org.lara.language.specification.ast.TypeDefNode; @@ -36,14 +46,10 @@ import org.lara.language.specification.dsl.types.ParameterizedType; import org.lara.language.specification.dsl.types.ThisType; import org.lara.language.specification.dsl.types.TypeDef; + import pt.up.fe.specs.util.SpecsSystem; import pt.up.fe.specs.util.providers.ResourceProvider; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; - /** * Comprehensive integration tests for the 'this' type and generic types feature. * diff --git a/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/WildcardTypeXmlIntegrationTest.java b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/WildcardTypeXmlIntegrationTest.java new file mode 100644 index 000000000..ce78430e4 --- /dev/null +++ b/LanguageSpecification/test/pt/up/fe/specs/lara/langspec/WildcardTypeXmlIntegrationTest.java @@ -0,0 +1,828 @@ +/** + * Copyright 2026 SPeCS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.lara.langspec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.lara.language.specification.dsl.Action; +import org.lara.language.specification.dsl.Attribute; +import org.lara.language.specification.dsl.JoinPointClass; +import org.lara.language.specification.dsl.LanguageSpecification; +import org.lara.language.specification.dsl.Parameter; +import org.lara.language.specification.dsl.types.ArrayType; +import org.lara.language.specification.dsl.types.IType; +import org.lara.language.specification.dsl.types.ParameterizedType; +import org.lara.language.specification.dsl.types.ThisType; + +import pt.up.fe.specs.util.providers.ResourceProvider; + +/** + * Comprehensive integration tests for the wildcard type feature in generic types. + * + *

Tests cover: + *

    + *
  • Basic wildcard types: List<?>, Set<?>
  • + *
  • Bounded wildcards: List<? extends this>, List<? super String>
  • + *
  • Nested wildcards: Map<?, List<?>>
  • + *
  • Wildcard arrays: List<?[]>, Set<? extends this[]>
  • + *
  • Multiple wildcards: Map<?, ?>, Map<? extends this, ? super String>
  • + *
  • Deeply nested generics with wildcards
  • + *
  • Actions returning wildcard types
  • + *
  • Actions with wildcard parameters
  • + *
  • Combinations of wildcards with 'this' type
  • + *
  • Inheritance preservation of wildcard types with 'this'
  • + *
+ */ +@DisplayName("WildcardType XML Integration Tests") +public class WildcardTypeXmlIntegrationTest { + + private static final String WILDCARD_BASE_PACKAGE = "pt/up/fe/specs/lara/langspec/wildcards/"; + + /** + * Resource provider enum for wildcard generic test resources. + */ + public enum WildcardTestResource implements ResourceProvider { + JOIN_POINT_MODEL("joinPointModel.xml"), + ATTRIBUTE_MODEL("artifacts.xml"), + ACTION_MODEL("actionModel.xml"); + + private final String resource; + + WildcardTestResource(String resource) { + this.resource = WILDCARD_BASE_PACKAGE + resource; + } + + @Override + public String getResource() { + return resource; + } + } + + + private LanguageSpecification parseWildcardSpec() { + return LangSpecsXmlParser.parse( + WildcardTestResource.JOIN_POINT_MODEL.toStream(), + WildcardTestResource.ATTRIBUTE_MODEL.toStream(), + WildcardTestResource.ACTION_MODEL.toStream(), + true + ); + } + + + // ==================== Wildcard Generic Types Tests ==================== + + @Nested + @DisplayName("Basic Wildcard Generic Types Parsing") + class BasicWildcardTests { + + @Test + @DisplayName("List is parsed correctly") + void testListWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute wildcardList = findAttribute(node.getAttributesSelf(), "wildcardList"); + + assertNotNull(wildcardList); + assertInstanceOf(ParameterizedType.class, wildcardList.getType()); + ParameterizedType paramType = (ParameterizedType) wildcardList.getType(); + + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + assertEquals("?", paramType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("List is parsed correctly") + void testListWildcardExtendsThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute extendsThisList = findAttribute(node.getAttributesSelf(), "extendsThisList"); + + assertNotNull(extendsThisList); + assertInstanceOf(ParameterizedType.class, extendsThisList.getType()); + ParameterizedType paramType = (ParameterizedType) extendsThisList.getType(); + + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + assertEquals("? extends this", paramType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("Map is parsed correctly") + void testMapStringWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute mapStringWildcard = findAttribute(node.getAttributesSelf(), "mapStringWildcard"); + + assertNotNull(mapStringWildcard); + assertInstanceOf(ParameterizedType.class, mapStringWildcard.getType()); + ParameterizedType paramType = (ParameterizedType) mapStringWildcard.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals("String", paramType.getTypeArguments().get(0).toString()); + assertEquals("?", paramType.getTypeArguments().get(1).toString()); + } + + @Test + @DisplayName("List is parsed correctly") + void testListWildcardSuperString() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute superStringList = findAttribute(node.getAttributesSelf(), "superStringList"); + + assertNotNull(superStringList); + assertInstanceOf(ParameterizedType.class, superStringList.getType()); + ParameterizedType paramType = (ParameterizedType) superStringList.getType(); + + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + assertEquals("? super String", paramType.getTypeArguments().get(0).toString()); + } + } + + // ==================== Nested Wildcards Tests ==================== + + @Nested + @DisplayName("Nested Wildcard Types") + class NestedWildcardTests { + + @Test + @DisplayName("Map> is parsed correctly") + void testNestedWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute nestedWildcard = findAttribute(node.getAttributesSelf(), "nestedWildcard"); + + assertNotNull(nestedWildcard); + assertInstanceOf(ParameterizedType.class, nestedWildcard.getType()); + ParameterizedType paramType = (ParameterizedType) nestedWildcard.getType(); + + assertEquals("Map>", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals("?", paramType.getTypeArguments().get(0).toString()); + + // Inner type should be List + IType innerType = paramType.getTypeArguments().get(1); + assertInstanceOf(ParameterizedType.class, innerType); + ParameterizedType innerParamType = (ParameterizedType) innerType; + assertEquals("List", innerParamType.toString()); + assertEquals("?", innerParamType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("List> is parsed correctly") + void testDeeplyNestedWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute deeplyNestedWildcard = findAttribute(node.getAttributesSelf(), "deeplyNestedWildcard"); + + assertNotNull(deeplyNestedWildcard); + assertInstanceOf(ParameterizedType.class, deeplyNestedWildcard.getType()); + ParameterizedType paramType = (ParameterizedType) deeplyNestedWildcard.getType(); + + assertEquals("List>", paramType.toString()); + + // Navigate to the innermost type + ParameterizedType mapType = (ParameterizedType) paramType.getTypeArguments().get(0); + assertEquals("Map", mapType.toString()); + assertEquals("String", mapType.getTypeArguments().get(0).toString()); + assertEquals("? extends this", mapType.getTypeArguments().get(1).toString()); + } + + @Test + @DisplayName("Map>> is parsed correctly (triple nested)") + void testTripleNestedWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute tripleNested = findAttribute(node.getAttributesSelf(), "tripleNested"); + + assertNotNull(tripleNested); + assertInstanceOf(ParameterizedType.class, tripleNested.getType()); + ParameterizedType paramType = (ParameterizedType) tripleNested.getType(); + + assertEquals("Map>>", paramType.toString()); + + // Navigate: Map -> List -> Set -> ? + ParameterizedType listType = (ParameterizedType) paramType.getTypeArguments().get(1); + assertEquals("List>", listType.toString()); + + ParameterizedType setType = (ParameterizedType) listType.getTypeArguments().get(0); + assertEquals("Set", setType.toString()); + assertEquals("?", setType.getTypeArguments().get(0).toString()); + } + } + + // ==================== Wildcard Arrays Tests ==================== + + @Nested + @DisplayName("Wildcard Array Types") + class WildcardArrayTests { + + @Test + @DisplayName("List is parsed correctly") + void testWildcardArray() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute wildcardArray = findAttribute(node.getAttributesSelf(), "wildcardArray"); + + assertNotNull(wildcardArray); + assertInstanceOf(ParameterizedType.class, wildcardArray.getType()); + ParameterizedType paramType = (ParameterizedType) wildcardArray.getType(); + + assertEquals("List", paramType.toString()); + assertEquals(1, paramType.getTypeArguments().size()); + + // The type argument should be an array of wildcards + IType typeArg = paramType.getTypeArguments().get(0); + assertInstanceOf(ArrayType.class, typeArg); + assertEquals("?[]", typeArg.toString()); + } + + @Test + @DisplayName("List is parsed correctly") + void testExtendsThisArray() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute extendsThisArray = findAttribute(node.getAttributesSelf(), "extendsThisArray"); + + assertNotNull(extendsThisArray); + assertInstanceOf(ParameterizedType.class, extendsThisArray.getType()); + ParameterizedType paramType = (ParameterizedType) extendsThisArray.getType(); + + assertEquals("List", paramType.toString()); + } + + @Test + @DisplayName("Set is parsed correctly") + void testSuperStringArray() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute superStringArray = findAttribute(node.getAttributesSelf(), "superStringArray"); + + assertNotNull(superStringArray); + assertInstanceOf(ParameterizedType.class, superStringArray.getType()); + ParameterizedType paramType = (ParameterizedType) superStringArray.getType(); + + assertEquals("Set", paramType.toString()); + } + } + + // ==================== Multiple Wildcards Tests ==================== + + @Nested + @DisplayName("Multiple Wildcards in Single Type") + class MultipleWildcardsTests { + + @Test + @DisplayName("Map is parsed correctly") + void testMapMultipleWildcards() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute multipleWildcards = findAttribute(node.getAttributesSelf(), "multipleWildcards"); + + assertNotNull(multipleWildcards); + assertInstanceOf(ParameterizedType.class, multipleWildcards.getType()); + ParameterizedType paramType = (ParameterizedType) multipleWildcards.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals("?", paramType.getTypeArguments().get(0).toString()); + assertEquals("?", paramType.getTypeArguments().get(1).toString()); + } + + @Test + @DisplayName("Map is parsed correctly") + void testMapMixedWildcards() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute mixedWildcards = findAttribute(node.getAttributesSelf(), "mixedWildcards"); + + assertNotNull(mixedWildcards); + assertInstanceOf(ParameterizedType.class, mixedWildcards.getType()); + ParameterizedType paramType = (ParameterizedType) mixedWildcards.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals("? extends this", paramType.getTypeArguments().get(0).toString()); + assertEquals("? super String", paramType.getTypeArguments().get(1).toString()); + } + } + + // ==================== Wildcards with 'this' Type Tests ==================== + + @Nested + @DisplayName("Wildcards Combined with 'this' Type") + class WildcardsWithThisTests { + + @Test + @DisplayName("Set is parsed correctly") + void testSetExtendsThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute setExtendsThis = findAttribute(node.getAttributesSelf(), "setExtendsThis"); + + assertNotNull(setExtendsThis); + assertInstanceOf(ParameterizedType.class, setExtendsThis.getType()); + ParameterizedType paramType = (ParameterizedType) setExtendsThis.getType(); + + assertEquals("Set", paramType.toString()); + assertEquals("? extends this", paramType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("Map combines 'this' and wildcard") + void testMapThisWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute mapThisWildcard = findAttribute(node.getAttributesSelf(), "mapThisWildcard"); + + assertNotNull(mapThisWildcard); + assertInstanceOf(ParameterizedType.class, mapThisWildcard.getType()); + ParameterizedType paramType = (ParameterizedType) mapThisWildcard.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(0)); + assertEquals("? super String", paramType.getTypeArguments().get(1).toString()); + } + + @Test + @DisplayName("Map combines wildcard and 'this'") + void testMapWildcardThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute mapWildcardThis = findAttribute(node.getAttributesSelf(), "mapWildcardThis"); + + assertNotNull(mapWildcardThis); + assertInstanceOf(ParameterizedType.class, mapWildcardThis.getType()); + ParameterizedType paramType = (ParameterizedType) mapWildcardThis.getType(); + + assertEquals("Map", paramType.toString()); + assertEquals(2, paramType.getTypeArguments().size()); + assertEquals("? extends String", paramType.getTypeArguments().get(0).toString()); + assertInstanceOf(ThisType.class, paramType.getTypeArguments().get(1)); + } + + @Test + @DisplayName("Map> combines complex types") + void testComplexNestedWildcardWithThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute complexNested = findAttribute(node.getAttributesSelf(), "complexNested"); + + assertNotNull(complexNested); + assertInstanceOf(ParameterizedType.class, complexNested.getType()); + ParameterizedType paramType = (ParameterizedType) complexNested.getType(); + + assertEquals("Map>", paramType.toString()); + + // First arg: ? extends this + assertEquals("? extends this", paramType.getTypeArguments().get(0).toString()); + + // Second arg: List + ParameterizedType listType = (ParameterizedType) paramType.getTypeArguments().get(1); + assertEquals("List", listType.toString()); + assertEquals("? super String", listType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("List> has wildcard with nested 'this'") + void testWildcardOfThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute wildcardOfThis = findAttribute(node.getAttributesSelf(), "wildcardOfThis"); + + assertNotNull(wildcardOfThis); + assertInstanceOf(ParameterizedType.class, wildcardOfThis.getType()); + ParameterizedType paramType = (ParameterizedType) wildcardOfThis.getType(); + + assertEquals("List>", paramType.toString()); + } + } + + // ==================== Actions Returning Wildcard Types ==================== + + @Nested + @DisplayName("Actions Returning Wildcard Types") + class ActionsReturningWildcardTests { + + @Test + @DisplayName("Action returning List") + void testActionReturningListWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getWildcardList = findAction(node.getActionsSelf(), "getWildcardList"); + + assertNotNull(getWildcardList); + assertEquals("List", getWildcardList.getReturnType()); + assertInstanceOf(ParameterizedType.class, getWildcardList.getType()); + ParameterizedType paramType = (ParameterizedType) getWildcardList.getType(); + assertEquals("?", paramType.getTypeArguments().get(0).toString()); + } + + @Test + @DisplayName("Action returning Set") + void testActionReturningExtendsThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getExtendsThis = findAction(node.getActionsSelf(), "getExtendsThis"); + + assertNotNull(getExtendsThis); + assertEquals("Set", getExtendsThis.getReturnType()); + assertInstanceOf(ParameterizedType.class, getExtendsThis.getType()); + } + + @Test + @DisplayName("Action returning List") + void testActionReturningSuperString() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getSuperString = findAction(node.getActionsSelf(), "getSuperString"); + + assertNotNull(getSuperString); + assertEquals("List", getSuperString.getReturnType()); + } + + @Test + @DisplayName("Action returning nested wildcard Map>") + void testActionReturningNestedWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getNestedWildcard = findAction(node.getActionsSelf(), "getNestedWildcard"); + + assertNotNull(getNestedWildcard); + assertEquals("Map>", getNestedWildcard.getReturnType()); + } + + @Test + @DisplayName("Action returning deeply nested List>") + void testActionReturningDeeplyNested() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getDeeplyNested = findAction(node.getActionsSelf(), "getDeeplyNested"); + + assertNotNull(getDeeplyNested); + assertEquals("List>", getDeeplyNested.getReturnType()); + } + + @Test + @DisplayName("Action returning List (wildcard array)") + void testActionReturningWildcardArray() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getWildcardArray = findAction(node.getActionsSelf(), "getWildcardArray"); + + assertNotNull(getWildcardArray); + assertEquals("List", getWildcardArray.getReturnType()); + } + + @Test + @DisplayName("Action returning Set") + void testActionReturningExtendsThisArray() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getExtendsThisArray = findAction(node.getActionsSelf(), "getExtendsThisArray"); + + assertNotNull(getExtendsThisArray); + assertEquals("Set", getExtendsThisArray.getReturnType()); + } + + @Test + @DisplayName("Action returning Map") + void testActionReturningMultiWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getMultiWildcard = findAction(node.getActionsSelf(), "getMultiWildcard"); + + assertNotNull(getMultiWildcard); + assertEquals("Map", getMultiWildcard.getReturnType()); + } + + @Test + @DisplayName("Action returning Map") + void testActionReturningMixedWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action getMixedWildcard = findAction(node.getActionsSelf(), "getMixedWildcard"); + + assertNotNull(getMixedWildcard); + assertEquals("Map", getMixedWildcard.getReturnType()); + } + } + + // ==================== Actions with Wildcard Parameters ==================== + + @Nested + @DisplayName("Actions with Wildcard Parameters") + class ActionsWithWildcardParamsTests { + + @Test + @DisplayName("Action with List parameter") + void testActionWithListWildcardParam() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processWildcardList = findAction(node.getActionsSelf(), "processWildcardList"); + + assertNotNull(processWildcardList); + assertEquals("void", processWildcardList.getReturnType()); + assertEquals(1, processWildcardList.getParameters().size()); + + Parameter param = processWildcardList.getParameters().get(0); + assertEquals("items", param.getName()); + assertEquals("List", param.getType()); + } + + @Test + @DisplayName("Action with Set parameter") + void testActionWithExtendsThisParam() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processExtendsThis = findAction(node.getActionsSelf(), "processExtendsThis"); + + assertNotNull(processExtendsThis); + assertEquals(1, processExtendsThis.getParameters().size()); + assertEquals("Set", processExtendsThis.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with List parameter") + void testActionWithSuperStringParam() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processSuperString = findAction(node.getActionsSelf(), "processSuperString"); + + assertNotNull(processSuperString); + assertEquals(1, processSuperString.getParameters().size()); + assertEquals("List", processSuperString.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with nested wildcard parameter Map>") + void testActionWithNestedWildcardParam() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processNestedWildcard = findAction(node.getActionsSelf(), "processNestedWildcard"); + + assertNotNull(processNestedWildcard); + assertEquals(1, processNestedWildcard.getParameters().size()); + assertEquals("Map>", processNestedWildcard.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with deeply nested parameter List>") + void testActionWithDeeplyNestedParam() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processDeeplyNested = findAction(node.getActionsSelf(), "processDeeplyNested"); + + assertNotNull(processDeeplyNested); + assertEquals(1, processDeeplyNested.getParameters().size()); + assertEquals("List>", processDeeplyNested.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action with multiple wildcard parameters") + void testActionWithMultipleWildcardParams() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action processMultipleWildcards = findAction(node.getActionsSelf(), "processMultipleWildcards"); + + assertNotNull(processMultipleWildcards); + assertEquals(3, processMultipleWildcards.getParameters().size()); + assertEquals("List", processMultipleWildcards.getParameters().get(0).getType()); + assertEquals("Set", processMultipleWildcards.getParameters().get(1).getType()); + assertEquals("Map", processMultipleWildcards.getParameters().get(2).getType()); + } + } + + // ==================== Actions with Wildcard Return AND Parameters ==================== + + @Nested + @DisplayName("Actions with Wildcard Return Types and Parameters") + class ActionsWithWildcardReturnAndParamsTests { + + @Test + @DisplayName("Action returning List with Set parameter") + void testTransformWildcard() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action transformWildcard = findAction(node.getActionsSelf(), "transformWildcard"); + + assertNotNull(transformWildcard); + assertEquals("List", transformWildcard.getReturnType()); + assertEquals(1, transformWildcard.getParameters().size()); + assertEquals("Set", transformWildcard.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Action returning Map with two wildcard parameters") + void testMergeWildcards() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action mergeWildcards = findAction(node.getActionsSelf(), "mergeWildcards"); + + assertNotNull(mergeWildcards); + assertEquals("Map", mergeWildcards.getReturnType()); + assertEquals(2, mergeWildcards.getParameters().size()); + assertEquals("Map", mergeWildcards.getParameters().get(0).getType()); + assertEquals("Map", mergeWildcards.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Action returning Map with List parameter") + void testCombineWithThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Action combineWithThis = findAction(node.getActionsSelf(), "combineWithThis"); + + assertNotNull(combineWithThis); + assertEquals("Map", combineWithThis.getReturnType()); + assertEquals(1, combineWithThis.getParameters().size()); + assertEquals("List", combineWithThis.getParameters().get(0).getType()); + + // Verify return type structure + ParameterizedType returnType = (ParameterizedType) combineWithThis.getType(); + assertInstanceOf(ThisType.class, returnType.getTypeArguments().get(0)); + assertEquals("? extends this", returnType.getTypeArguments().get(1).toString()); + } + } + + // ==================== Inheritance Tests for Wildcards with 'this' ==================== + + @Nested + @DisplayName("Inheritance Preservation of Wildcard Types with 'this'") + class InheritanceTests { + + @Test + @DisplayName("Wildcard with 'this' is preserved in inherited attribute") + void testWildcardThisPreservedInInheritedAttribute() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + JoinPointClass expr = wildcardSpec.getJoinPoint("expr"); + + // Find extendsThisList on node (where it's defined) + Attribute extendsOnNode = findAttribute(node.getAttributesSelf(), "extendsThisList"); + assertNotNull(extendsOnNode); + assertEquals("List", extendsOnNode.getType().toString()); + + // Find it on expr (inherited) + Attribute extendsOnExpr = findAttribute(expr.getAttributes(), "extendsThisList"); + assertNotNull(extendsOnExpr); + + // Should still be the same type string (late-bound) + assertEquals("List", extendsOnExpr.getType().toString()); + } + + @Test + @DisplayName("Expr has its own wildcard attribute with 'this'") + void testExprHasOwnWildcardAttribute() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass expr = wildcardSpec.getJoinPoint("expr"); + + Attribute exprSpecific = findAttribute(expr.getAttributesSelf(), "exprSpecific"); + assertNotNull(exprSpecific); + assertEquals("Set", exprSpecific.getType().toString()); + } + + @Test + @DisplayName("Expr inherits all node's wildcard attributes") + void testExprInheritsNodeWildcardAttributes() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass expr = wildcardSpec.getJoinPoint("expr"); + + // Should have access to all of node's wildcard attributes via inheritance + assertNotNull(findAttribute(expr.getAttributes(), "wildcardList")); + assertNotNull(findAttribute(expr.getAttributes(), "extendsThisList")); + assertNotNull(findAttribute(expr.getAttributes(), "mapStringWildcard")); + assertNotNull(findAttribute(expr.getAttributes(), "multipleWildcards")); + assertNotNull(findAttribute(expr.getAttributes(), "nestedWildcard")); + } + + @Test + @DisplayName("Expr-specific action returns wildcard with 'this'") + void testExprActionReturnsWildcardWithThis() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass expr = wildcardSpec.getJoinPoint("expr"); + + Action exprWildcard = findAction(expr.getActionsSelf(), "exprWildcard"); + assertNotNull(exprWildcard); + assertEquals("Set", exprWildcard.getReturnType()); + } + } + + // ==================== Attributes with Parameters Tests ==================== + + @Nested + @DisplayName("Attributes with Wildcard Parameters") + class AttributesWithParametersTests { + + @Test + @DisplayName("Attribute returning wildcard with simple parameter") + void testAttributeWithSimpleParameter() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute filterByType = findAttribute(node.getAttributesSelf(), "filterByType"); + + assertNotNull(filterByType); + assertEquals("List", filterByType.getType().toString()); + assertEquals(1, filterByType.getParameters().size()); + assertEquals("typeName", filterByType.getParameters().get(0).getName()); + assertEquals("String", filterByType.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Attribute returning wildcard with wildcard parameter") + void testAttributeWithWildcardParameter() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute findMatching = findAttribute(node.getAttributesSelf(), "findMatching"); + + assertNotNull(findMatching); + assertEquals("Set", findMatching.getType().toString()); + assertEquals(1, findMatching.getParameters().size()); + assertEquals("criteria", findMatching.getParameters().get(0).getName()); + assertEquals("Map", findMatching.getParameters().get(0).getType()); + } + + @Test + @DisplayName("Attribute with multiple wildcard 'this' parameters") + void testAttributeWithMultipleWildcardThisParameters() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute searchBetween = findAttribute(node.getAttributesSelf(), "searchBetween"); + + assertNotNull(searchBetween); + assertEquals("List", searchBetween.getType().toString()); + assertEquals(2, searchBetween.getParameters().size()); + assertEquals("start", searchBetween.getParameters().get(0).getName()); + assertEquals("? extends this", searchBetween.getParameters().get(0).getType()); + assertEquals("end", searchBetween.getParameters().get(1).getName()); + assertEquals("? extends this", searchBetween.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Attribute with complex wildcard parameters") + void testAttributeWithComplexWildcardParameters() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute transformWith = findAttribute(node.getAttributesSelf(), "transformWith"); + + assertNotNull(transformWith); + assertEquals("Map", transformWith.getType().toString()); + assertEquals(2, transformWith.getParameters().size()); + assertEquals("transformer", transformWith.getParameters().get(0).getName()); + assertEquals("List", transformWith.getParameters().get(0).getType()); + assertEquals("options", transformWith.getParameters().get(1).getName()); + assertEquals("Map", transformWith.getParameters().get(1).getType()); + } + + @Test + @DisplayName("Attribute with contravariant wildcard parameter (? super this)") + void testAttributeWithContravariantWildcardParameter() { + LanguageSpecification wildcardSpec = parseWildcardSpec(); + JoinPointClass node = wildcardSpec.getJoinPoint("node"); + Attribute collectInto = findAttribute(node.getAttributesSelf(), "collectInto"); + + assertNotNull(collectInto); + assertEquals("List", collectInto.getType().toString()); + assertEquals(1, collectInto.getParameters().size()); + assertEquals("target", collectInto.getParameters().get(0).getName()); + assertEquals("List", collectInto.getParameters().get(0).getType()); + } + } + + // ==================== Helper Methods ==================== + + private static Attribute findAttribute(List attributes, String name) { + return attributes.stream() + .filter(a -> name.equals(a.getName())) + .findFirst() + .orElse(null); + } + + private static Action findAction(List actions, String name) { + return actions.stream() + .filter(a -> name.equals(a.getName())) + .findFirst() + .orElse(null); + } +} From 867132825d54a1406ecb706e0a2dfc7ccc51e366 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 31 Jan 2026 04:52:59 +0000 Subject: [PATCH 03/14] Add validation to prevent wildcard bounds from being wildcard types --- .../lara/language/specification/dsl/types/WildcardType.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java b/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java index 7e3fc22a5..7ba584ce4 100644 --- a/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java +++ b/LanguageSpecification/src/org/lara/language/specification/dsl/types/WildcardType.java @@ -90,6 +90,9 @@ public WildcardType(Kind kind, IType bound) { if (kind != Kind.UNBOUNDED && bound == null) { throw new IllegalArgumentException("Bounded wildcard (" + kind + ") requires a bound type"); } + if (bound instanceof WildcardType) { + throw new IllegalArgumentException("Wildcard bounds cannot be wildcard types"); + } this.kind = kind; this.bound = bound; From 12f307a265dad8a1eb612e86f5728f60531b0527 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Mon, 2 Mar 2026 21:50:51 +0000 Subject: [PATCH 04/14] feat: Add support for 'this' type and generics in various entities and actions - Implemented exception handling with ThistypeWeaverException for better error reporting. - Updated XML specifications of integration tests for actions and artifacts to test 'this' type and generics. - Enhanced Java code generation tests to ensure 'this' type integration and method signature uniqueness. - Added regression tests to verify correct behavior of generated code and prevent duplicate signatures. - Developed utility tests for conversion behavior related to 'this' type in the context of attribute conversion. --- .../resources/specification/artifacts.xml | 3 + .../specification/joinPointModel.xml | 16 +- .../defaultweaver/joinpoints/DWFile.java | 2 +- .../defaultweaver/joinpoints/DWFolder.java | 2 +- .../defaultweaver/joinpoints/DWFunction.java | 13 +- .../defaultweaver/joinpoints/DWMethod.java | 38 + .../defaultweaver/joinpoints/DWorkspace.java | 2 +- .../dsl/LanguageSpecification.java | 26 + .../language/specification/dsl/Parameter.java | 15 + .../lara/langspec/LangSpecsXmlParser.java | 11 +- .../dsl/types/WildcardTypeTest.java | 14 +- WeaverGenerator/CRTP_PROTOTYPE.md | 145 +++ .../AbstractJoinPointClassGenerator.java | 99 ++- .../SuperAbstractJoinPointGenerator.java | 78 +- .../helpers/UserAbstractJPClassGenerator.java | 9 +- .../java/helpers/UserEntitiesGenerator.java | 9 +- .../generator/java/utils/ConvertUtils.java | 348 ++++++-- .../generator/java/utils/CrtpJavaClass.java | 161 ++++ .../generator/java/utils/GeneratorUtils.java | 430 +++++++-- .../abstracts/AEdgeWeaverJoinPoint.java.txt | 2 +- .../pkg/abstracts/joinpoints/ABase.java.txt | 2 +- .../abstracts/joinpoints/AJoinPoint.java.txt | 2 +- .../pkg/abstracts/joinpoints/ALevel1.java.txt | 6 +- .../pkg/abstracts/joinpoints/ALevel2.java.txt | 6 +- .../joinpoints/AReservedKeyword.java.txt | 6 +- .../abstracts/AMediumWeaverJoinPoint.java.txt | 2 +- .../pkg/abstracts/joinpoints/ABody.java.txt | 2 +- .../pkg/abstracts/joinpoints/AFile.java.txt | 2 +- .../abstracts/joinpoints/AFunction.java.txt | 2 +- .../abstracts/joinpoints/AJoinPoint.java.txt | 2 +- .../abstracts/joinpoints/AStatement.java.txt | 2 +- .../pkg/abstracts/joinpoints/AVar.java.txt | 2 +- .../AMinimalWeaverJoinPoint.java.txt | 2 +- .../abstracts/joinpoints/AJoinPoint.java.txt | 2 +- .../pkg/abstracts/joinpoints/ARoot.java.txt | 2 +- .../thistype/pkg/ThistypeWeaver.java.txt | 94 ++ .../AThistypeWeaverJoinPoint.java.txt | 38 + .../abstracts/joinpoints/ABinaryExpr.java.txt | 684 +++++++++++++++ .../abstracts/joinpoints/AContainer.java.txt | 147 ++++ .../pkg/abstracts/joinpoints/AExpr.java.txt | 648 ++++++++++++++ .../abstracts/joinpoints/AJoinPoint.java.txt | 204 +++++ .../pkg/abstracts/joinpoints/ALoop.java.txt | 742 ++++++++++++++++ .../pkg/abstracts/joinpoints/ANode.java.txt | 827 ++++++++++++++++++ .../pkg/abstracts/joinpoints/AStmt.java.txt | 629 +++++++++++++ .../abstracts/weaver/AThistypeWeaver.java.txt | 66 ++ .../thistype/pkg/entities/AstContext.java.txt | 24 + .../thistype/pkg/entities/NodeInfo.java.txt | 109 +++ .../pkg/entities/SimpleMetadata.java.txt | 89 ++ .../pkg/entities/TreeStructure.java.txt | 109 +++ .../thistype/pkg/enums/NodeKind.java.txt | 46 + .../ThistypeWeaverException.java.txt | 46 + .../spec/valid/thistype/actionModel.xml | 166 ++++ .../spec/valid/thistype/artifacts.xml | 160 ++++ .../spec/valid/thistype/joinPointModel.xml | 29 + .../codegen/JavaCodegenGoldenTest.java | 102 +++ .../codegen/JavaCodegenRegressionTest.java | 83 ++ .../generator/e2e/GeneratorE2ETest.java | 6 + .../java/utils/ConvertUtilsTest.java | 85 ++ 58 files changed, 6352 insertions(+), 246 deletions(-) create mode 100644 DefaultWeaver/src/org/lara/interpreter/weaver/defaultweaver/joinpoints/DWMethod.java create mode 100644 WeaverGenerator/CRTP_PROTOTYPE.md create mode 100644 WeaverGenerator/src/org/lara/interpreter/weaver/generator/generator/java/utils/CrtpJavaClass.java create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/ThistypeWeaver.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/AThistypeWeaverJoinPoint.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/ABinaryExpr.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/AContainer.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/AExpr.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/AJoinPoint.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/ALoop.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/ANode.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/joinpoints/AStmt.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/abstracts/weaver/AThistypeWeaver.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/entities/AstContext.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/entities/NodeInfo.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/entities/SimpleMetadata.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/entities/TreeStructure.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/enums/NodeKind.java.txt create mode 100644 WeaverGenerator/test-resources/golden/thistype/pkg/exceptions/ThistypeWeaverException.java.txt create mode 100644 WeaverGenerator/test-resources/spec/valid/thistype/actionModel.xml create mode 100644 WeaverGenerator/test-resources/spec/valid/thistype/artifacts.xml create mode 100644 WeaverGenerator/test-resources/spec/valid/thistype/joinPointModel.xml create mode 100644 WeaverGenerator/test/org/lara/interpreter/weaver/generator/codegen/JavaCodegenRegressionTest.java create mode 100644 WeaverGenerator/test/org/lara/interpreter/weaver/generator/generator/java/utils/ConvertUtilsTest.java diff --git a/DefaultWeaver/resources/specification/artifacts.xml b/DefaultWeaver/resources/specification/artifacts.xml index b7a8b45be..fd299e53a 100644 --- a/DefaultWeaver/resources/specification/artifacts.xml +++ b/DefaultWeaver/resources/specification/artifacts.xml @@ -14,5 +14,8 @@ + + + \ No newline at end of file diff --git a/DefaultWeaver/resources/specification/joinPointModel.xml b/DefaultWeaver/resources/specification/joinPointModel.xml index e933066fa..aee5cdeb0 100644 --- a/DefaultWeaver/resources/specification/joinPointModel.xml +++ b/DefaultWeaver/resources/specification/joinPointModel.xml @@ -1,13 +1,9 @@ - - - - - -