Skip to content

Commit

Permalink
feat: scoping of direct references to declarations in same file (#580)
Browse files Browse the repository at this point in the history
Closes partially #540

### Summary of Changes

Resolve the target of references if
* it is in the same file as the declaration
* the reference is not the member of a member access.

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lars-reimann and megalinter-bot authored Sep 28, 2023
1 parent 2e6be9f commit 491d7b0
Show file tree
Hide file tree
Showing 37 changed files with 1,897 additions and 18 deletions.
8 changes: 4 additions & 4 deletions src/language/ast/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const classMembersOrEmpty = function (node: SdsClass | undefined): SdsCla
return node?.body?.members ?? [];
};

export const enumVariantsOrEmpty = function (node: SdsEnum | undefined): SdsEnumVariant[] {
return node?.body?.variants ?? [];
};

export const parametersOrEmpty = function (node: SdsParameterList | undefined): SdsParameter[] {
return node?.parameters ?? [];
};
Expand Down Expand Up @@ -90,7 +94,3 @@ export const typeArgumentsOrEmpty = function (node: SdsTypeArgumentList | undefi
export const typeParametersOrEmpty = function (node: SdsTypeParameterList | undefined): SdsTypeParameter[] {
return node?.typeParameters ?? [];
};

export const variantsOrEmpty = function (node: SdsEnum | undefined): SdsEnumVariant[] {
return node?.body?.variants ?? [];
};
8 changes: 8 additions & 0 deletions src/language/ast/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AstNode, hasContainerOfType } from 'langium';

/**
* Returns whether the inner node is contained in the outer node. If the nodes are equal, this function returns `true`.
*/
export const isContainedIn = (inner: AstNode | undefined, outer: AstNode | undefined): boolean => {
return hasContainerOfType(inner, (it) => it === outer);
};
4 changes: 2 additions & 2 deletions src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -757,11 +757,11 @@ SdsString returns SdsString:
;

interface SdsReference extends SdsExpression {
declaration?: @SdsDeclaration
target?: @SdsDeclaration
}

SdsReference returns SdsReference:
declaration=[SdsDeclaration:ID]
target=[SdsDeclaration:ID]
;

interface SdsParenthesizedExpression extends SdsExpression {
Expand Down
2 changes: 1 addition & 1 deletion src/language/partialEvaluation/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class SdsIntermediateRecord extends SdsIntermediateExpression {
}

getSubstitutionByReferenceOrNull(reference: SdsReference): SdsSimplifiedExpression | null {
const referencedDeclaration = reference.declaration;
const referencedDeclaration = reference.target;
if (!isSdsAbstractResult(referencedDeclaration)) {
return null;
}
Expand Down
230 changes: 224 additions & 6 deletions src/language/scoping/safe-ds-scope-provider.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
import { DefaultScopeProvider, EMPTY_SCOPE, getContainerOfType, ReferenceInfo, Scope } from 'langium';
import {
AstNode,
DefaultScopeProvider,
EMPTY_SCOPE,
getContainerOfType,
getDocument,
ReferenceInfo,
Scope,
} from 'langium';
import {
isSdsAssignment,
isSdsBlock,
isSdsCallable,
isSdsClass,
isSdsEnum,
isSdsLambda,
isSdsMemberAccess,
isSdsMemberType,
isSdsModule,
isSdsNamedType,
isSdsNamedTypeDeclaration,
isSdsPlaceholder,
isSdsReference,
isSdsSegment,
isSdsStatement,
isSdsYield,
SdsMemberAccess,
SdsMemberType,
SdsNamedTypeDeclaration,
SdsPlaceholder,
SdsReference,
SdsStatement,
SdsType,
SdsYield,
} from '../generated/ast.js';
import { resultsOrEmpty } from '../ast/shortcuts.js';
import { assigneesOrEmpty, parametersOrEmpty, resultsOrEmpty, statementsOrEmpty } from '../ast/shortcuts.js';
import { isContainedIn } from '../ast/utils.js';

export class SafeDsScopeProvider extends DefaultScopeProvider {
override getScope(context: ReferenceInfo): Scope {
if (isSdsNamedType(context.container) && context.property === 'declaration') {
const node = context.container;
const node = context.container;

if (isSdsNamedType(node) && context.property === 'declaration') {
if (isSdsMemberType(node.$container) && node.$containerProperty === 'member') {
return this.getScopeForMemberTypeMember(node.$container);
} else {
return super.getScope(context);
}
} else if (isSdsYield(context.container) && context.property === 'result') {
return this.getScopeForYieldResult(context.container);
} else if (isSdsReference(node) && context.property === 'target') {
if (isSdsMemberAccess(node.$container) && node.$containerProperty === 'member') {
return this.getScopeForMemberAccessMember(node.$container);
} else {
return this.getScopeForDirectReferenceTarget(node);
}
} else if (isSdsYield(node) && context.property === 'result') {
return this.getScopeForYieldResult(node);
} else {
return super.getScope(context);
}
Expand Down Expand Up @@ -65,6 +93,196 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
}
}

private getScopeForMemberAccessMember(_node: SdsMemberAccess): Scope {
return EMPTY_SCOPE;
}

private getScopeForDirectReferenceTarget(node: SdsReference): Scope {
// Declarations in this file
const currentScope = this.globalDeclarationsInSameFile(node, EMPTY_SCOPE);

// Declarations in containing blocks
return this.localDeclarations(node, currentScope);
}

private globalDeclarationsInSameFile(node: AstNode, outerScope: Scope): Scope {
const module = getContainerOfType(node, isSdsModule);
if (!module) {
/* c8 ignore next 2 */
return outerScope;
}

const precomputed = getDocument(module).precomputedScopes?.get(module);
if (!precomputed) {
/* c8 ignore next 2 */
return outerScope;
}

return this.createScope(precomputed, outerScope);
}

private localDeclarations(node: AstNode, outerScope: Scope): Scope {
// Parameters
const containingCallable = getContainerOfType(node.$container, isSdsCallable);
const parameters = parametersOrEmpty(containingCallable?.parameterList);

// Placeholders up to the containing statement
const containingStatement = getContainerOfType(node.$container, isSdsStatement);

let placeholders: Iterable<SdsPlaceholder>;
if (!containingCallable || isContainedIn(containingStatement, containingCallable)) {
placeholders = this.placeholdersUpToStatement(containingStatement);
} else {
// Placeholders are further away than the parameters
placeholders = [];
}

// Local declarations
const localDeclarations = [...parameters, ...placeholders];

// Lambdas can be nested
if (isSdsLambda(containingCallable)) {
return this.createScopeForNodes(localDeclarations, this.localDeclarations(containingCallable, outerScope));
} else {
return this.createScopeForNodes(localDeclarations, outerScope);
}
}

private *placeholdersUpToStatement(
statement: SdsStatement | undefined,
): Generator<SdsPlaceholder, void, undefined> {
if (!statement) {
return;
}

const containingBlock = getContainerOfType(statement, isSdsBlock);
for (const current of statementsOrEmpty(containingBlock)) {
if (current === statement) {
return;
}

if (isSdsAssignment(current)) {
yield* assigneesOrEmpty(current).filter(isSdsPlaceholder);
}
}
}

// private fun scopeForReferenceDeclaration(context: SdsReference): IScope {
// val resource = context.eResource()
// val packageName = context.containingCompilationUnitOrNull()?.qualifiedNameOrNull()
//
// // Declarations in other files
// var result: IScope = FilteringScope(
// super.delegateGetScope(context, SafeDSPackage.Literals.SDS_REFERENCE__DECLARATION),
// ) {
// it.isReferencableExternalDeclaration(resource, packageName)
// }
//
// // Declarations in this file
// result = declarationsInSameFile(resource, result)
//
// // Declarations in containing classes
// context.containingClassOrNull()?.let {
// result = classMembers(it, result)
// }
//
// // Declarations in containing blocks
// localDeclarations(context, result)
// }
// }
// }
//
// /**
// * Removes declarations in this [Resource], [SdsAnnotation]s, and internal [SdsStep]s located in other
// * [SdsCompilationUnit]s.
// */
// private fun IEObjectDescription?.isReferencableExternalDeclaration(
// fromResource: Resource,
// fromPackageWithQualifiedName: QualifiedName?,
// ): Boolean {
// // Resolution failed in delegate scope
// if (this == null) return false
//
// val obj = this.eObjectOrProxy
//
// // Local declarations are added later using custom scoping rules
// if (obj.eResource() == fromResource) return false
//
// // Annotations cannot be referenced
// if (obj is SdsAnnotation) return false
//
// // Internal steps in another package cannot be referenced
// return !(
// obj is SdsStep &&
// obj.visibility() == SdsVisibility.Internal &&
// obj.containingCompilationUnitOrNull()?.qualifiedNameOrNull() != fromPackageWithQualifiedName
// )
// }
//
// private fun scopeForMemberAccessDeclaration(context: SdsMemberAccess): IScope {
// val receiver = context.receiver
//
// // Static access
// val receiverDeclaration = when (receiver) {
// is SdsReference -> receiver.declaration
// is SdsMemberAccess -> receiver.member.declaration
// else -> null
// }
// if (receiverDeclaration != null) {
// when (receiverDeclaration) {
// is SdsClass -> {
// val members = receiverDeclaration.classMembersOrEmpty().filter { it.isStatic() }
// val superTypeMembers = receiverDeclaration.superClassMembers()
// .filter { it.isStatic() }
// .toList()
//
// return Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers))
// }
// is SdsEnum -> {
// return Scopes.scopeFor(receiverDeclaration.variantsOrEmpty())
// }
// }
// }
//
// // Call results
// var resultScope = IScope.NULLSCOPE
// if (receiver is SdsCall) {
// val results = receiver.resultsOrNull()
// when {
// results == null -> return IScope.NULLSCOPE
// results.size > 1 -> return Scopes.scopeFor(results)
// results.size == 1 -> resultScope = Scopes.scopeFor(results)
// }
// }
//
// // 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
// }
// }
//
// private fun classMembers(context: SdsClass, parentScope: IScope): IScope {
// return when (val containingClassOrNull = context.containingClassOrNull()) {
// is SdsClass -> Scopes.scopeFor(
// context.classMembersOrEmpty(),
// classMembers(containingClassOrNull, parentScope),
// )
// else -> Scopes.scopeFor(context.classMembersOrEmpty(), parentScope)
// }
// }

private getScopeForYieldResult(node: SdsYield): Scope {
const containingSegment = getContainerOfType(node, isSdsSegment);
if (!containingSegment) {
Expand Down
4 changes: 2 additions & 2 deletions src/language/validation/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
placeholdersOrEmpty,
resultsOrEmpty,
typeParametersOrEmpty,
variantsOrEmpty,
enumVariantsOrEmpty,
} from '../ast/shortcuts.js';

export const CODE_NAME_BLOCK_LAMBDA_PREFIX = 'name/block-lambda-prefix';
Expand Down Expand Up @@ -185,7 +185,7 @@ export const classMustContainUniqueNames = (node: SdsClass, accept: ValidationAc
};

export const enumMustContainUniqueNames = (node: SdsEnum, accept: ValidationAcceptor): void => {
namesMustBeUnique(variantsOrEmpty(node), (name) => `A variant with name '${name}' exists already.`, accept);
namesMustBeUnique(enumVariantsOrEmpty(node), (name) => `A variant with name '${name}' exists already.`, accept);
};

export const enumVariantMustContainUniqueNames = (node: SdsEnumVariant, accept: ValidationAcceptor): void => {
Expand Down
28 changes: 28 additions & 0 deletions src/language/validation/other/expressions/references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { isSdsAnnotation, isSdsPipeline, isSdsSchema, SdsReference } from '../../../generated/ast.js';
import { ValidationAcceptor } from 'langium';

export const CODE_REFERENCE_TARGET = 'reference/target';

export const referenceTargetMustNotBeAnnotationPipelineOrSchema = (
node: SdsReference,
accept: ValidationAcceptor,
): void => {
const target = node.target?.ref;

if (isSdsAnnotation(target)) {
accept('error', 'An annotation must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
} else if (isSdsPipeline(target)) {
accept('error', 'A pipeline must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
} else if (isSdsSchema(target)) {
accept('error', 'A schema must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
}
};
2 changes: 2 additions & 0 deletions src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { callableTypeMustNotHaveOptionalParameters } from './other/types/callabl
import { typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/types/typeArgumentLists.js';
import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js';
import { parameterMustNotBeVariadicAndOptional } from './other/declarations/parameters.js';
import { referenceTargetMustNotBeAnnotationPipelineOrSchema } from './other/expressions/references.js';

/**
* Register custom validation checks.
Expand Down Expand Up @@ -75,6 +76,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
parameterListVariadicParameterMustBeLast,
],
SdsPipeline: [pipelineMustContainUniqueNames],
SdsReference: [referenceTargetMustNotBeAnnotationPipelineOrSchema],
SdsResult: [resultMustHaveTypeHint],
SdsSegment: [segmentMustContainUniqueNames, segmentResultListShouldNotBeEmpty],
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const getNodeByLocation = (services: SafeDsServices, location: Location):
});
}

for (const node of streamAllContents(root)) {
for (const node of streamAllContents(root, { range: location.range })) {
// Entire node matches the range
const actualRange = node.$cstNode?.range;
if (actualRange && isRangeEqual(actualRange, location.range)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tests.partialValidation.baseCases.stringLiteralWithInterpolation
package tests.partialValidation.simpleRecursiveCases.templateString

pipeline test {
// $TEST$ constant serialization "start 1 inner1 true inner2 test end"
Expand Down
Loading

0 comments on commit 491d7b0

Please sign in to comment.