Skip to content

Commit

Permalink
feat: validation for annotation target (#670)
Browse files Browse the repository at this point in the history
Closes #543

### Summary of Changes

* Show an error if an annotation is called on an incorrect target
* Show a warning if the target list of an annotation has duplicate
entries
  • Loading branch information
lars-reimann committed Oct 22, 2023
1 parent ba1e9a8 commit fa7631d
Show file tree
Hide file tree
Showing 30 changed files with 1,739 additions and 65 deletions.
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

0 comments on commit fa7631d

Please sign in to comment.