diff --git a/src/language/grammar/safe-ds.langium b/src/language/grammar/safe-ds.langium index a97cfb619..6806c5f2b 100644 --- a/src/language/grammar/safe-ds.langium +++ b/src/language/grammar/safe-ds.langium @@ -685,10 +685,10 @@ SdsChainedExpression returns SdsExpression: {SdsCall.receiver=current} argumentList=SdsCallArgumentList - | {SdsIndexedAccess.receiver=current} + | {SdsIndexedAccess.receiver=current} '[' index=SdsExpression ']' - | {SdsMemberAccess.receiver=current} + | {SdsMemberAccess.receiver=current} (isNullSafe?='?')? '.' member=SdsReference diff --git a/src/language/scoping/safe-ds-scope-provider.ts b/src/language/scoping/safe-ds-scope-provider.ts index eb45363b0..9c9ec45a2 100644 --- a/src/language/scoping/safe-ds-scope-provider.ts +++ b/src/language/scoping/safe-ds-scope-provider.ts @@ -67,6 +67,7 @@ import { SafeDsServices } from '../safe-ds-module.js'; import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js'; import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; +import { ClassType, EnumVariantType } from '../typing/model.js'; export class SafeDsScopeProvider extends DefaultScopeProvider { private readonly astReflection: AstReflection; @@ -176,13 +177,13 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { // Static access const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver); if (isSdsClass(declaration)) { - return this.createScopeForNodes(classMembersOrEmpty(declaration, isStatic)); - - // val superTypeMembers = receiverDeclaration.superClassMembers() - // .filter { it.isStatic() } - // .toList() + const ownStaticMembers = classMembersOrEmpty(declaration, isStatic); + // val superTypeMembers = receiverDeclaration.superClassMembers() + // .filter { it.isStatic() } + // .toList() // - // return Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers)) + // return Scopes.scopeFor(ownStaticMembers, Scopes.scopeFor(superTypeMembers)) + return this.createScopeForNodes(ownStaticMembers); } else if (isSdsEnum(declaration)) { return this.createScopeForNodes(enumVariantsOrEmpty(declaration)); } @@ -193,9 +194,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { const callable = this.nodeMapper.callToCallableOrUndefined(node.receiver); const results = abstractResultsOrEmpty(callable); - if (results.length === 0) { - return EMPTY_SCOPE; - } else if (results.length > 1) { + if (results.length > 1) { return this.createScopeForNodes(results); } else { // If there is only one result, it can be accessed by name but members of the result with the same name @@ -204,22 +203,24 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { } } - // // Members - // val type = (receiver.type() as? NamedType) ?: return resultScope - // - // return when { - // type.isNullable && !context.isNullSafe -> resultScope - // type is ClassType -> { - // val members = type.sdsClass.classMembersOrEmpty().filter { !it.isStatic() } - // val superTypeMembers = type.sdsClass.superClassMembers() - // .filter { !it.isStatic() } - // .toList() - // - // Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers, resultScope)) - // } - // type is EnumVariantType -> Scopes.scopeFor(type.sdsEnumVariant.parametersOrEmpty()) - // else -> resultScope - // } + // Members + let receiverType = this.typeComputer.computeType(node.receiver); + if (receiverType.isNullable && !node.isNullSafe) { + return resultScope; + } + + if (receiverType instanceof ClassType) { + const ownInstanceMembers = classMembersOrEmpty(receiverType.sdsClass, (it) => !isStatic(it)); + // val superTypeMembers = type.sdsClass.superClassMembers() + // .filter { !it.isStatic() } + // .toList() + // + // Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers, resultScope)) + return this.createScopeForNodes(ownInstanceMembers, resultScope); + } else if (receiverType instanceof EnumVariantType) { + const parameters = parametersOrEmpty(receiverType.sdsEnumVariant); + return this.createScopeForNodes(parameters, resultScope); + } return resultScope; } diff --git a/src/language/typing/model.ts b/src/language/typing/model.ts index 16e190d7d..d24de68a5 100644 --- a/src/language/typing/model.ts +++ b/src/language/typing/model.ts @@ -12,6 +12,10 @@ import { export abstract class Type { abstract isNullable: boolean; + unwrap(): Type { + return this; + } + abstract copyWithNullability(isNullable: boolean): Type; abstract equals(other: Type): boolean; @@ -110,6 +114,17 @@ export class NamedTupleType extends Type { return this.entries.length; } + /** + * If this only has one entry, returns its type. Otherwise, returns this. + */ + override unwrap(): Type { + if (this.entries.length === 1) { + return this.entries[0].type; + } + + return this; + } + override copyWithNullability(_isNullable: boolean): NamedTupleType { return this; } diff --git a/src/language/typing/safe-ds-type-computer.ts b/src/language/typing/safe-ds-type-computer.ts index ebdf168cd..3ee0abfd8 100644 --- a/src/language/typing/safe-ds-type-computer.ts +++ b/src/language/typing/safe-ds-type-computer.ts @@ -104,7 +104,7 @@ export class SafeDsTypeComputer { const documentUri = getDocument(node).uri.toString(); const nodePath = this.astNodeLocator.getAstNodePath(node); const key = `${documentUri}~${nodePath}`; - return this.typeCache.get(key, () => this.doComputeType(node)); + return this.typeCache.get(key, () => this.doComputeType(node).unwrap()); } // fun SdsAbstractObject.hasPrimitiveType(): Boolean { diff --git a/tests/resources/scoping/member accesses/to class members/instance attributes/main.sdstest b/tests/resources/scoping/member accesses/to class members/instance attributes/main.sdstest index a5f3b8379..f7699f7a9 100644 --- a/tests/resources/scoping/member accesses/to class members/instance attributes/main.sdstest +++ b/tests/resources/scoping/member accesses/to class members/instance attributes/main.sdstest @@ -49,13 +49,57 @@ class MyClass { class AnotherClass +fun nullableMyClass() -> result: MyClass? + pipeline myPipeline { + // $TEST$ references myInstanceAttribute + val myClass = MyClass(); + myClass.»myInstanceAttribute«; + + + // $TEST$ references redeclaredAsInstanceAttribute + MyClass().»redeclaredAsInstanceAttribute«; + + // $TEST$ references redeclaredAsStaticAttribute + MyClass().»redeclaredAsStaticAttribute«; + + // $TEST$ references redeclaredAsNestedClass + MyClass().»redeclaredAsNestedClass«; + + // $TEST$ references redeclaredAsNestedEnum + MyClass().»redeclaredAsNestedEnum«; + + // $TEST$ references redeclaredAsInstanceMethod + MyClass().»redeclaredAsInstanceMethod«; + + // $TEST$ references redeclaredAsStaticMethod + MyClass().»redeclaredAsStaticMethod«; + + // $TEST$ references declaredPreviouslyAsStaticAttribute + MyClass().»declaredPreviouslyAsStaticAttribute«; + + // $TEST$ references declaredPreviouslyAsNestedClass + MyClass().»declaredPreviouslyAsNestedClass«; + + // $TEST$ references declaredPreviouslyAsNestedEnum + MyClass().»declaredPreviouslyAsNestedEnum«; + + // $TEST$ references declaredPreviouslyAsStaticMethod + MyClass().»declaredPreviouslyAsStaticMethod«; + + // $TEST$ references myInstanceAttribute + nullableMyClass()?.»myInstanceAttribute«; + + // $TEST$ unresolved MyClass.»myInstanceAttribute«; // $TEST$ unresolved AnotherClass().»myInstanceAttribute«; + // $TEST$ unresolved + nullableMyClass().»myInstanceAttribute«; + // $TEST$ unresolved unresolved.»myInstanceAttribute«; diff --git a/tests/resources/scoping/member accesses/to class members/instance methods/main.sdstest b/tests/resources/scoping/member accesses/to class members/instance methods/main.sdstest index 695e6ba1e..693bdbf49 100644 --- a/tests/resources/scoping/member accesses/to class members/instance methods/main.sdstest +++ b/tests/resources/scoping/member accesses/to class members/instance methods/main.sdstest @@ -49,13 +49,57 @@ class MyClass { class AnotherClass +fun nullableMyClass() -> result: MyClass? + pipeline myPipeline { + // $TEST$ references myInstanceMethod + val myClass = MyClass(); + myClass.»myInstanceMethod«(); + + + // $TEST$ references redeclaredAsInstanceAttribute + MyClass().»redeclaredAsInstanceAttribute«(); + + // $TEST$ references redeclaredAsStaticAttribute + MyClass().»redeclaredAsStaticAttribute«(); + + // $TEST$ references redeclaredAsNestedClass + MyClass().»redeclaredAsNestedClass«(); + + // $TEST$ references redeclaredAsNestedEnum + MyClass().»redeclaredAsNestedEnum«(); + + // $TEST$ references redeclaredAsInstanceMethod + MyClass().»redeclaredAsInstanceMethod«(); + + // $TEST$ references redeclaredAsStaticMethod + MyClass().»redeclaredAsStaticMethod«(); + + // $TEST$ references declaredPreviouslyAsStaticAttribute + MyClass().»declaredPreviouslyAsStaticAttribute«(); + + // $TEST$ references declaredPreviouslyAsNestedClass + MyClass().»declaredPreviouslyAsNestedClass«(); + + // $TEST$ references declaredPreviouslyAsNestedEnum + MyClass().»declaredPreviouslyAsNestedEnum«(); + + // $TEST$ references declaredPreviouslyAsStaticMethod + MyClass().»declaredPreviouslyAsStaticMethod«(); + + // $TEST$ references myInstanceMethod + nullableMyClass()?.»myInstanceMethod«(); + + // $TEST$ unresolved MyClass.»myInstanceMethod«; // $TEST$ unresolved AnotherClass().»myInstanceMethod«; + // $TEST$ unresolved + nullableMyClass().»myInstanceAttribute«; + // $TEST$ unresolved unresolved.»myInstanceMethod«; diff --git a/tests/resources/scoping/member accesses/to parameter of enum variants/main.sdstest b/tests/resources/scoping/member accesses/to parameter of enum variants/main.sdstest new file mode 100644 index 000000000..2ba689a18 --- /dev/null +++ b/tests/resources/scoping/member accesses/to parameter of enum variants/main.sdstest @@ -0,0 +1,45 @@ +package tests.scoping.memberAccesses.toParametersOfEnumVariants + +enum MyEnum { + MyEnumVariant( + // $TEST$ target param + »param«: Int, + + // $TEST$ target redeclared + »redeclared«: Int, + redeclared: Int, + ) + + MyOtherEnumVariant +} + +enum MyOtherEnum { + MyEnumVariant +} + +pipeline myPipeline { + // $TEST$ references param + MyEnum.MyEnumVariant().»param«; + + // $TEST$ references redeclared + MyEnum.MyEnumVariant().»redeclared«; + + + // $TEST$ unresolved + MyOtherEnum.MyEnumVariant.»param«; + + // $TEST$ unresolved + MyEnum.MyOtherEnumVariant().»param«; + + // $TEST$ unresolved + MyOtherEnum.MyEnumVariant().»param«; + + // $TEST$ unresolved + MyEnum.MyEnumVariant().»unresolved«; + + // $TEST$ unresolved + MyEnum.unresolved().»param«; + + // $TEST$ unresolved + unresolved.MyEnumVariant().»param«; +} diff --git a/tests/resources/scoping/member accesses/to results/skip-of block lambdas (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/of block lambdas (matching member)/main.sdstest similarity index 50% rename from tests/resources/scoping/member accesses/to results/skip-of block lambdas (matching member)/main.sdstest rename to tests/resources/scoping/member accesses/to results/of block lambdas (matching member)/main.sdstest index fb53b4374..b197f34bc 100644 --- a/tests/resources/scoping/member accesses/to results/skip-of block lambdas (matching member)/main.sdstest +++ b/tests/resources/scoping/member accesses/to results/of block lambdas (matching member)/main.sdstest @@ -5,11 +5,23 @@ class MyClass() { attr »result«: Int } +enum MyEnum { + // $TEST$ target MyEnum_result + MyEnumVariant(»result«: Int) +} + pipeline myPipeline { - val lambdaWithOneResultWithIdenticalMember = () { + val f1 = () { yield result = MyClass(); }; + val f2 = () { + yield result = MyEnum.MyEnumVariant(0); + }; + // $TEST$ references MyClass_result - lambdaWithOneResultWithIdenticalMember().»result«; + f1().»result«; + + // $TEST$ references MyEnum_result + f2().»result«; } diff --git a/tests/resources/scoping/member accesses/to results/of callable types (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/of callable types (matching member)/main.sdstest new file mode 100644 index 000000000..a7cc1c823 --- /dev/null +++ b/tests/resources/scoping/member accesses/to results/of callable types (matching member)/main.sdstest @@ -0,0 +1,23 @@ +package tests.scoping.memberAccesses.toResults.ofCallableTypes.matchingMember + +class MyClass() { + // $TEST$ target MyClass_result + attr »result«: Int +} + +enum MyEnum { + // $TEST$ target MyEnum_result + MyEnumVariant(»result«: Int) +} + +segment mySegment( + f1: () -> result: MyClass, + f2: () -> result: MyEnum.MyEnumVariant, +) { + + // $TEST$ references MyClass_result + f1().»result«; + + // $TEST$ references MyEnum_result + f2().»result«; +} diff --git a/tests/resources/scoping/member accesses/to results/of functions (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/of functions (matching member)/main.sdstest new file mode 100644 index 000000000..263e0eece --- /dev/null +++ b/tests/resources/scoping/member accesses/to results/of functions (matching member)/main.sdstest @@ -0,0 +1,22 @@ +package tests.scoping.memberAccesses.toResults.ofFunctions.matchingMember + +class MyClass() { + // $TEST$ target MyClass_result + attr »result«: Int +} + +enum MyEnum { + // $TEST$ target MyEnum_result + MyEnumVariant(»result«: Int) +} + +fun f1() -> result: MyClass +fun f2() -> result: MyEnum.MyEnumVariant + +pipeline myPipeline { + // $TEST$ references MyClass_result + f1().»result«; + + // $TEST$ references MyEnum_result + f2().»result«; +} diff --git a/tests/resources/scoping/member accesses/to results/of segments (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/of segments (matching member)/main.sdstest new file mode 100644 index 000000000..79463aaeb --- /dev/null +++ b/tests/resources/scoping/member accesses/to results/of segments (matching member)/main.sdstest @@ -0,0 +1,22 @@ +package tests.scoping.memberAccesses.toResults.ofSegments.matchingMember + +class MyClass() { + // $TEST$ target MyClass_result + attr »result«: Int +} + +enum MyEnum { + // $TEST$ target MyEnum_result + MyEnumVariant(»result«: Int) +} + +segment s1() -> result: MyClass {} +segment s2() -> result: MyEnum.MyEnumVariant {} + +pipeline myPipeline { + // $TEST$ references MyClass_result + s1().»result«; + + // $TEST$ references MyEnum_result + s2().»result«; +} diff --git a/tests/resources/scoping/member accesses/to results/skip-of callable types (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/skip-of callable types (matching member)/main.sdstest deleted file mode 100644 index 3d14a472b..000000000 --- a/tests/resources/scoping/member accesses/to results/skip-of callable types (matching member)/main.sdstest +++ /dev/null @@ -1,13 +0,0 @@ -package tests.scoping.memberAccesses.toResults.ofCallableTypes.matchingMember - -class MyClass() { - // $TEST$ target MyClass_result - attr »result«: Int -} - -segment mySegment( - callableWithIdenticalMember: () -> result: MyClass -) { - // $TEST$ references MyClass_result - callableWithIdenticalMember().»result«; -} diff --git a/tests/resources/scoping/member accesses/to results/skip-of functions (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/skip-of functions (matching member)/main.sdstest deleted file mode 100644 index 6ac04e80e..000000000 --- a/tests/resources/scoping/member accesses/to results/skip-of functions (matching member)/main.sdstest +++ /dev/null @@ -1,13 +0,0 @@ -package tests.scoping.memberAccesses.toResults.ofFunctions.matchingMember - -class MyClass() { - // $TEST$ target MyClass_result - attr »result«: Int -} - -fun functionWithOneResultWithIdenticalMember() -> result: MyClass - -pipeline myPipeline { - // $TEST$ references MyClass_result - functionWithOneResultWithIdenticalMember().»result«; -} diff --git a/tests/resources/scoping/member accesses/to results/skip-of segments (matching member)/main.sdstest b/tests/resources/scoping/member accesses/to results/skip-of segments (matching member)/main.sdstest deleted file mode 100644 index c90dae517..000000000 --- a/tests/resources/scoping/member accesses/to results/skip-of segments (matching member)/main.sdstest +++ /dev/null @@ -1,13 +0,0 @@ -package tests.scoping.memberAccesses.toResults.ofSegments.matchingMember - -class MyClass() { - // $TEST$ target MyClass_result - attr »result«: Int -} - -segment segmentWithOneResultWithIdenticalMember() -> result: MyClass {} - -pipeline myPipeline { - // $TEST$ references MyClass_result - segmentWithOneResultWithIdenticalMember().»result«; -} diff --git a/tests/resources/typing/expressions/calls/of block lambdas/main.sdstest b/tests/resources/typing/expressions/calls/of block lambdas/main.sdstest index c6c4b418c..eacbaeceb 100644 --- a/tests/resources/typing/expressions/calls/of block lambdas/main.sdstest +++ b/tests/resources/typing/expressions/calls/of block lambdas/main.sdstest @@ -1,7 +1,7 @@ package tests.typing.expressions.calls.ofBlockLambdas pipeline myPipeline { - // $TEST$ serialization (r: String) + // $TEST$ serialization String »(() { yield r = ""; })()«; diff --git a/tests/resources/typing/expressions/calls/of callable types/main.sdstest b/tests/resources/typing/expressions/calls/of callable types/main.sdstest index 032cbec00..4cc957bb3 100644 --- a/tests/resources/typing/expressions/calls/of callable types/main.sdstest +++ b/tests/resources/typing/expressions/calls/of callable types/main.sdstest @@ -4,7 +4,7 @@ segment mySegment( p1: () -> r: String, p2: () -> (r: String, s: Int) ) { - // $TEST$ serialization (r: String) + // $TEST$ serialization String »p1()«; // $TEST$ serialization (r: String, s: Int) diff --git a/tests/resources/typing/expressions/calls/of expression lambdas/main.sdstest b/tests/resources/typing/expressions/calls/of expression lambdas/main.sdstest index 7866aa463..d634db366 100644 --- a/tests/resources/typing/expressions/calls/of expression lambdas/main.sdstest +++ b/tests/resources/typing/expressions/calls/of expression lambdas/main.sdstest @@ -1,6 +1,6 @@ package tests.typing.expressions.calls.ofExpressionLambdas pipeline myPipeline { - // $TEST$ serialization (result: Int) + // $TEST$ serialization Int »(() -> 1)()«; } diff --git a/tests/resources/typing/expressions/calls/of functions/main.sdstest b/tests/resources/typing/expressions/calls/of functions/main.sdstest index e044b7e0a..dec1e1e1d 100644 --- a/tests/resources/typing/expressions/calls/of functions/main.sdstest +++ b/tests/resources/typing/expressions/calls/of functions/main.sdstest @@ -4,7 +4,7 @@ fun f1() -> r: String fun f2() -> (r: String, s: Int) pipeline myPipeline { - // $TEST$ serialization (r: String) + // $TEST$ serialization String »f1()«; // $TEST$ serialization (r: String, s: Int) diff --git a/tests/resources/typing/expressions/calls/of segments/main.sdstest b/tests/resources/typing/expressions/calls/of segments/main.sdstest index 99f805821..4c8861ea4 100644 --- a/tests/resources/typing/expressions/calls/of segments/main.sdstest +++ b/tests/resources/typing/expressions/calls/of segments/main.sdstest @@ -9,7 +9,7 @@ segment s2() -> (r: String, s: Int) { } pipeline myPipeline { - // $TEST$ serialization (r: String) + // $TEST$ serialization String »s1()«; // $TEST$ serialization (r: String, s: Int) diff --git a/tests/resources/validation/style/unnecessary safe access/main.sdstest b/tests/resources/validation/style/unnecessary safe access/main.sdstest index c1ad2d81c..35154efb8 100644 --- a/tests/resources/validation/style/unnecessary safe access/main.sdstest +++ b/tests/resources/validation/style/unnecessary safe access/main.sdstest @@ -1,10 +1,22 @@ package tests.validation.style.unnecessarySafeAccess +class MyClass + +fun nullableMyClass() -> result: MyClass? + pipeline test { // $TEST$ info "The receiver is never null, so the safe access is unnecessary." »1?.toString«(); + // $TEST$ no info "The receiver is never null, so the safe access is unnecessary." »null?.toString«(); + + // $TEST$ no info "The receiver is never null, so the safe access is unnecessary." + »nullableMyClass()?.toString«(); + + // $TEST$ no info "The receiver is never null, so the safe access is unnecessary." + nullableMyClass()»?.toString«(); // Langium currently computes the wrong range for a member access + // $TEST$ no info "The receiver is never null, so the safe access is unnecessary." »unresolved?.toString«(); }