Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validation for annotation target #670

Merged
merged 5 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions src/language/builtins/safe-ds-annotations.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsModule, SdsParameter } from '../generated/ast.js';
import { getArguments, findFirstAnnotationCallOf, hasAnnotationCallOf } from '../helpers/nodeProperties.js';
import {
isSdsAnnotation,
isSdsEnum,
SdsAnnotatedObject,
SdsAnnotation,
SdsEnumVariant,
SdsModule,
SdsParameter,
} from '../generated/ast.js';
import {
findFirstAnnotationCallOf,
getArguments,
getEnumVariants,
getParameters,
hasAnnotationCallOf,
} from '../helpers/nodeProperties.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';
import { resourceNameToUri } from '../../helpers/resources.js';
import { URI } from 'langium';
import { EMPTY_STREAM, getContainerOfType, Stream, stream, URI } from 'langium';
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { EvaluatedNode, StringConstant } from '../partialEvaluation/model.js';
import { EvaluatedEnumVariant, EvaluatedList, EvaluatedNode, StringConstant } from '../partialEvaluation/model.js';
import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js';
import { SafeDsEnums } from './safe-ds-enums.js';

const ANNOTATION_USAGE_URI = resourceNameToUri('builtins/safeds/lang/annotationUsage.sdsstub');
const CODE_GENERATION_URI = resourceNameToUri('builtins/safeds/lang/codeGeneration.sdsstub');
const IDE_INTEGRATION_URI = resourceNameToUri('builtins/safeds/lang/ideIntegration.sdsstub');
const MATURITY_URI = resourceNameToUri('builtins/safeds/lang/maturity.sdsstub');

export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
private readonly builtinEnums: SafeDsEnums;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly partialEvaluator: SafeDsPartialEvaluator;

constructor(services: SafeDsServices) {
super(services);

this.builtinEnums = services.builtins.Enums;
this.nodeMapper = services.helpers.NodeMapper;
this.partialEvaluator = services.evaluation.PartialEvaluator;
}
Expand Down Expand Up @@ -82,6 +99,32 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
return this.getAnnotation(ANNOTATION_USAGE_URI, 'Repeatable');
}

streamValidTargets(node: SdsAnnotation | undefined): Stream<SdsEnumVariant> {
// If no targets are specified, every target is valid
if (!hasAnnotationCallOf(node, this.Target)) {
return stream(getEnumVariants(this.builtinEnums.AnnotationTarget));
}

// If targets are specified, but we could not evaluate them to a list, no target is valid
const value = this.getArgumentValue(node, this.Target, 'targets');
if (!(value instanceof EvaluatedList)) {
return EMPTY_STREAM;
}

// Otherwise, filter the elements of the list and keep only variants of the AnnotationTarget enum
return stream(value.elements)
.filter(
(it) =>
it instanceof EvaluatedEnumVariant &&
getContainerOfType(it.variant, isSdsEnum) === this.builtinEnums.AnnotationTarget,
)
.map((it) => (<EvaluatedEnumVariant>it).variant);
}

get Target(): SdsAnnotation | undefined {
return this.getAnnotation(ANNOTATION_USAGE_URI, 'Target');
}

private getAnnotation(uri: URI, name: string): SdsAnnotation | undefined {
return this.getModuleMember(uri, name, isSdsAnnotation);
}
Expand All @@ -96,9 +139,17 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
parameterName: string,
): EvaluatedNode {
const annotationCall = findFirstAnnotationCallOf(node, annotation);
const argumentValue = getArguments(annotationCall).find(

// Parameter is set explicitly
const argument = getArguments(annotationCall).find(
(it) => this.nodeMapper.argumentToParameter(it)?.name === parameterName,
)?.value;
return this.partialEvaluator.evaluate(argumentValue);
);
if (argument) {
return this.partialEvaluator.evaluate(argument.value);
}

// Parameter is not set explicitly, so we use the default value
const parameter = getParameters(annotation).find((it) => it.name === parameterName);
return this.partialEvaluator.evaluate(parameter?.defaultValue);
}
}
16 changes: 16 additions & 0 deletions src/language/builtins/safe-ds-enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isSdsEnum, SdsEnum } from '../generated/ast.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';
import { resourceNameToUri } from '../../helpers/resources.js';
import { URI } from 'langium';

const ANNOTATION_USAGE_URI = resourceNameToUri('builtins/safeds/lang/annotationUsage.sdsstub');

export class SafeDsEnums extends SafeDsModuleMembers<SdsEnum> {
get AnnotationTarget(): SdsEnum | undefined {
return this.getEnum(ANNOTATION_USAGE_URI, 'AnnotationTarget');
}

private getEnum(uri: URI, name: string): SdsEnum | undefined {
return this.getModuleMember(uri, name, isSdsEnum);
}
}
4 changes: 4 additions & 0 deletions src/language/helpers/nodeProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export const getAnnotationCalls = (node: SdsAnnotatedObject | undefined): SdsAnn
}
};

export const getAnnotationCallTarget = (node: SdsAnnotationCall | undefined): SdsDeclaration | undefined => {
return getContainerOfType(node, isSdsDeclaration);
};

export const findFirstAnnotationCallOf = (
node: SdsAnnotatedObject | undefined,
expected: SdsAnnotation | undefined,
Expand Down
3 changes: 3 additions & 0 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { SafeDsCoreTypes } from './typing/safe-ds-core-types.js';
import { SafeDsNodeKindProvider } from './lsp/safe-ds-node-kind-provider.js';
import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-provider.js';
import { SafeDsDocumentBuilder } from './workspace/safe-ds-document-builder.js';
import { SafeDsEnums } from './builtins/safe-ds-enums.js';

/**
* Declaration of custom services - add your own service classes here.
Expand All @@ -37,6 +38,7 @@ export type SafeDsAddedServices = {
builtins: {
Annotations: SafeDsAnnotations;
Classes: SafeDsClasses;
Enums: SafeDsEnums;
};
evaluation: {
PartialEvaluator: SafeDsPartialEvaluator;
Expand Down Expand Up @@ -70,6 +72,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
builtins: {
Annotations: (services) => new SafeDsAnnotations(services),
Classes: (services) => new SafeDsClasses(services),
Enums: (services) => new SafeDsEnums(services),
},
evaluation: {
PartialEvaluator: (services) => new SafeDsPartialEvaluator(services),
Expand Down
156 changes: 156 additions & 0 deletions src/language/validation/builtins/target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ValidationAcceptor } from 'langium';
import {
isSdsAnnotation,
isSdsAttribute,
isSdsClass,
isSdsEnum,
isSdsEnumVariant,
isSdsFunction,
isSdsModule,
isSdsParameter,
isSdsPipeline,
isSdsResult,
isSdsSegment,
isSdsTypeParameter,
SdsAnnotation,
SdsAnnotationCall,
} from '../../generated/ast.js';
import { SafeDsServices } from '../../safe-ds-module.js';
import { duplicatesBy, isEmpty } from '../../../helpers/collectionUtils.js';
import { pluralize } from '../../../helpers/stringUtils.js';
import { findFirstAnnotationCallOf, getAnnotationCallTarget } from '../../helpers/nodeProperties.js';

export const CODE_TARGET_DUPLICATE_TARGET = 'target/duplicate-target';
export const CODE_TARGET_WRONG_TARGET = 'target/wrong-target';

export const targetShouldNotHaveDuplicateEntries = (services: SafeDsServices) => {
const builtinAnnotations = services.builtins.Annotations;

return (node: SdsAnnotation, accept: ValidationAcceptor) => {
const annotationCall = findFirstAnnotationCallOf(node, builtinAnnotations.Target);
if (!annotationCall) {
return;
}

const validTargets = builtinAnnotations.streamValidTargets(node).map((it) => `'${it.name}'`);
const duplicateTargets = duplicatesBy(validTargets, (it) => it)
.distinct()
.toArray();

if (isEmpty(duplicateTargets)) {
return;
}

const noun = pluralize(duplicateTargets.length, 'target');
const duplicateTargetString = duplicateTargets.join(', ');
const verb = pluralize(duplicateTargets.length, 'occurs', 'occur');

accept('warning', `The ${noun} ${duplicateTargetString} ${verb} multiple times.`, {
node: annotationCall,
property: 'annotation',
code: CODE_TARGET_DUPLICATE_TARGET,
});
};
};

export const annotationCallMustHaveCorrectTarget = (services: SafeDsServices) => {
const builtinAnnotations = services.builtins.Annotations;

return (node: SdsAnnotationCall, accept: ValidationAcceptor) => {
const annotation = node.annotation?.ref;
if (!annotation) {
return;
}

const actualTarget = getActualTarget(node);
/* c8 ignore start */
if (!actualTarget) {
return;
}
/* c8 ignore stop */

const validTargets = builtinAnnotations
.streamValidTargets(annotation)
.map((it) => it.name)
.toSet();

if (!validTargets.has(actualTarget.enumVariantName)) {
accept('error', `The annotation '${annotation.name}' cannot be applied to ${actualTarget.prettyName}.`, {
node,
property: 'annotation',
code: CODE_TARGET_WRONG_TARGET,
});
}
};
};

const getActualTarget = (node: SdsAnnotationCall): GetActualTargetResult | void => {
const annotatedObject = getAnnotationCallTarget(node);

if (isSdsAnnotation(annotatedObject)) {
return {
enumVariantName: 'Annotation',
prettyName: 'an annotation',
};
} else if (isSdsAttribute(annotatedObject)) {
return {
enumVariantName: 'Attribute',
prettyName: 'an attribute',
};
} else if (isSdsClass(annotatedObject)) {
return {
enumVariantName: 'Class',
prettyName: 'a class',
};
} else if (isSdsEnum(annotatedObject)) {
return {
enumVariantName: 'Enum',
prettyName: 'an enum',
};
} else if (isSdsEnumVariant(annotatedObject)) {
return {
enumVariantName: 'EnumVariant',
prettyName: 'an enum variant',
};
} else if (isSdsFunction(annotatedObject)) {
return {
enumVariantName: 'Function',
prettyName: 'a function',
};
} else if (isSdsModule(annotatedObject)) {
return {
enumVariantName: 'Module',
prettyName: 'a module',
};
} else if (isSdsParameter(annotatedObject)) {
return {
enumVariantName: 'Parameter',
prettyName: 'a parameter',
};
} else if (isSdsPipeline(annotatedObject)) {
return {
enumVariantName: 'Pipeline',
prettyName: 'a pipeline',
};
} else if (isSdsResult(annotatedObject)) {
return {
enumVariantName: 'Result',
prettyName: 'a result',
};
} else if (isSdsSegment(annotatedObject)) {
return {
enumVariantName: 'Segment',
prettyName: 'a segment',
};
} else if (isSdsTypeParameter(annotatedObject)) {
return {
enumVariantName: 'TypeParameter',
prettyName: 'a type parameter',
};
}
};

interface GetActualTargetResult {
enumVariantName: string;
prettyName: string;
}
3 changes: 3 additions & 0 deletions src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ import {
literalTypeMustNotContainMapLiteral,
literalTypeShouldNotHaveDuplicateLiteral,
} from './other/types/literalTypes.js';
import { annotationCallMustHaveCorrectTarget, targetShouldNotHaveDuplicateEntries } from './builtins/target.js';

/**
* Register custom validation checks.
Expand All @@ -150,12 +151,14 @@ export const registerValidationChecks = function (services: SafeDsServices) {
annotationMustContainUniqueNames,
annotationParameterListShouldNotBeEmpty,
annotationParameterShouldNotHaveConstModifier,
targetShouldNotHaveDuplicateEntries(services),
],
SdsAnnotationCall: [
annotationCallAnnotationShouldNotBeDeprecated(services),
annotationCallAnnotationShouldNotBeExperimental(services),
annotationCallArgumentListShouldBeNeeded,
annotationCallArgumentsMustBeConstant(services),
annotationCallMustHaveCorrectTarget(services),
annotationCallMustNotLackArgumentList,
],
SdsArgument: [
Expand Down
Loading