Skip to content

Commit

Permalink
feat: warn if deprecated/experimental declarations are used (#608)
Browse files Browse the repository at this point in the history
Closes partially #543
Closes partially #540

### Summary of Changes

Show a warning if deprecated or experimental declarations are used. To
implement this, I've also added scoping for member accesses to results.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot authored Oct 6, 2023
1 parent d53bda3 commit 9b5287c
Show file tree
Hide file tree
Showing 38 changed files with 1,148 additions and 408 deletions.
34 changes: 34 additions & 0 deletions src/language/builtins/safe-ds-annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { resolveRelativePathToBuiltinFile } from './fileFinder.js';
import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation } from '../generated/ast.js';
import { annotationCallsOrEmpty } from '../helpers/nodeProperties.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';

const CORE_ANNOTATIONS_URI = resolveRelativePathToBuiltinFile('safeds/lang/coreAnnotations.sdsstub');

export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
isDeprecated(node: SdsAnnotatedObject | undefined): boolean {
return annotationCallsOrEmpty(node).some((it) => {
const annotation = it.annotation?.ref;
return annotation === this.Deprecated;
});
}

isExperimental(node: SdsAnnotatedObject | undefined): boolean {
return annotationCallsOrEmpty(node).some((it) => {
const annotation = it.annotation?.ref;
return annotation === this.Experimental;
});
}

private get Deprecated(): SdsAnnotation | undefined {
return this.getAnnotation('Deprecated');
}

private get Experimental(): SdsAnnotation | undefined {
return this.getAnnotation('Experimental');
}

private getAnnotation(name: string): SdsAnnotation | undefined {
return this.getModuleMember(CORE_ANNOTATIONS_URI, name, isSdsAnnotation);
}
}
38 changes: 38 additions & 0 deletions src/language/builtins/safe-ds-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { resolveRelativePathToBuiltinFile } from './fileFinder.js';
import { isSdsClass, SdsClass } from '../generated/ast.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';

const CORE_CLASSES_URI = resolveRelativePathToBuiltinFile('safeds/lang/coreClasses.sdsstub');

export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
/* c8 ignore start */
get Any(): SdsClass | undefined {
return this.getClass('Any');
}

/* c8 ignore stop */

get Boolean(): SdsClass | undefined {
return this.getClass('Boolean');
}

get Float(): SdsClass | undefined {
return this.getClass('Float');
}

get Int(): SdsClass | undefined {
return this.getClass('Int');
}

get Nothing(): SdsClass | undefined {
return this.getClass('Nothing');
}

get String(): SdsClass | undefined {
return this.getClass('String');
}

private getClass(name: string): SdsClass | undefined {
return this.getModuleMember(CORE_CLASSES_URI, name, isSdsClass);
}
}
94 changes: 0 additions & 94 deletions src/language/builtins/safe-ds-core-classes.ts

This file was deleted.

43 changes: 43 additions & 0 deletions src/language/builtins/safe-ds-module-members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SafeDsServices } from '../safe-ds-module.js';
import { isSdsModule, SdsModuleMember } from '../generated/ast.js';
import { LangiumDocuments, URI, WorkspaceCache } from 'langium';
import { moduleMembersOrEmpty } from '../helpers/nodeProperties.js';

export abstract class SafeDsModuleMembers<T extends SdsModuleMember> {
private readonly langiumDocuments: LangiumDocuments;
private readonly cache: WorkspaceCache<string, T>;

constructor(services: SafeDsServices) {
this.langiumDocuments = services.shared.workspace.LangiumDocuments;
this.cache = new WorkspaceCache(services.shared);
}

protected getModuleMember(uri: URI, name: string, predicate: (node: unknown) => node is T): T | undefined {
const key = `${uri.toString()}#${name}`;

if (this.cache.has(key)) {
return this.cache.get(key);
}

if (!this.langiumDocuments.hasDocument(uri)) {
/* c8 ignore next 2 */
return undefined;
}

const document = this.langiumDocuments.getOrCreateDocument(uri);
const root = document.parseResult.value;
if (!isSdsModule(root)) {
/* c8 ignore next 2 */
return undefined;
}

const firstMatchingModuleMember = moduleMembersOrEmpty(root).find((m) => m.name === name);
if (!predicate(firstMatchingModuleMember)) {
/* c8 ignore next 2 */
return undefined;
}

this.cache.set(key, firstMatchingModuleMember);
return firstMatchingModuleMember;
}
}
2 changes: 1 addition & 1 deletion src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ fragment SdsSegmentFragment:
interface SdsAnnotationCallList extends SdsAnnotatedObject {}

interface SdsAnnotationCall extends SdsAbstractCall {
annotation?: @SdsAnnotation
annotation: @SdsAnnotation
}

SdsAnnotationCall returns SdsAnnotationCall:
Expand Down
21 changes: 21 additions & 0 deletions src/language/helpers/nodeProperties.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
isSdsAssignment,
isSdsAttribute,
isSdsBlockLambda,
isSdsBlockLambdaResult,
isSdsCallableType,
isSdsClass,
isSdsDeclaration,
isSdsEnum,
Expand All @@ -13,6 +15,7 @@ import {
isSdsSegment,
isSdsTypeParameterList,
SdsAbstractCall,
SdsAbstractResult,
SdsAnnotatedObject,
SdsAnnotationCall,
SdsArgument,
Expand Down Expand Up @@ -84,6 +87,24 @@ export const isStatic = (node: SdsClassMember): boolean => {
// Accessors for list elements
// -------------------------------------------------------------------------------------------------

export const abstractResultsOrEmpty = (node: SdsCallable | undefined): SdsAbstractResult[] => {
if (!node) {
return [];
}

if (isSdsBlockLambda(node)) {
return blockLambdaResultsOrEmpty(node);
} else if (isSdsCallableType(node)) {
return resultsOrEmpty(node.resultList);
} else if (isSdsFunction(node)) {
return resultsOrEmpty(node.resultList);
} else if (isSdsSegment(node)) {
return resultsOrEmpty(node.resultList);
} /* c8 ignore start */ else {
return [];
} /* c8 ignore stop */
};

export const annotationCallsOrEmpty = (node: SdsAnnotatedObject | undefined): SdsAnnotationCall[] => {
if (!node) {
/* c8 ignore next 2 */
Expand Down
39 changes: 39 additions & 0 deletions src/language/helpers/safe-ds-node-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import {
isSdsAbstractCall,
isSdsAnnotationCall,
isSdsAssignment,
isSdsBlock,
isSdsCall,
isSdsCallable,
Expand All @@ -12,8 +13,11 @@ import {
isSdsType,
isSdsYield,
SdsAbstractCall,
SdsAbstractResult,
SdsArgument,
SdsAssignee,
SdsCallable,
SdsExpression,
SdsParameter,
SdsPlaceholder,
SdsReference,
Expand All @@ -25,6 +29,7 @@ import {
import { CallableType, StaticType } from '../typing/model.js';
import { findLocalReferences, getContainerOfType } from 'langium';
import {
abstractResultsOrEmpty,
argumentsOrEmpty,
isNamedArgument,
isNamedTypeArgument,
Expand Down Expand Up @@ -81,6 +86,40 @@ export class SafeDsNodeMapper {
return undefined;
}

/**
* Returns the result, block lambda result, or expression that is assigned to the given assignee. If nothing is
* assigned, `undefined` is returned.
*/
assigneeToAssignedObjectOrUndefined(node: SdsAssignee | undefined): SdsAbstractResult | SdsExpression | undefined {
if (!node) {
return undefined;
}

const containingAssignment = getContainerOfType(node, isSdsAssignment);
/* c8 ignore start */
if (!containingAssignment) {
return undefined;
}
/* c8 ignore stop */

const assigneePosition = node.$containerIndex ?? -1;
const expression = containingAssignment.expression;

// If the RHS is not a call, the first assignee gets the entire RHS
if (!isSdsCall(expression)) {
if (assigneePosition === 0) {
return expression;
} else {
return undefined;
}
}

// If the RHS is a call, the assignee gets the corresponding result
const callable = this.callToCallableOrUndefined(expression);
const abstractResults = abstractResultsOrEmpty(callable);
return abstractResults[assigneePosition];
}

/**
* Returns the callable that is called by the given call. If no callable can be found, returns undefined.
*/
Expand Down
9 changes: 6 additions & 3 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ import { SafeDsScopeComputation } from './scoping/safe-ds-scope-computation.js';
import { SafeDsScopeProvider } from './scoping/safe-ds-scope-provider.js';
import { SafeDsValueConverter } from './grammar/safe-ds-value-converter.js';
import { SafeDsTypeComputer } from './typing/safe-ds-type-computer.js';
import { SafeDsCoreClasses } from './builtins/safe-ds-core-classes.js';
import { SafeDsClasses } from './builtins/safe-ds-classes.js';
import { SafeDsPackageManager } from './workspace/safe-ds-package-manager.js';
import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js';
import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js';

/**
* Declaration of custom services - add your own service classes here.
*/
export type SafeDsAddedServices = {
builtins: {
CoreClasses: SafeDsCoreClasses;
Annotations: SafeDsAnnotations;
Classes: SafeDsClasses;
};
helpers: {
NodeMapper: SafeDsNodeMapper;
Expand All @@ -52,7 +54,8 @@ export type SafeDsServices = LangiumServices & SafeDsAddedServices;
*/
export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeDsAddedServices> = {
builtins: {
CoreClasses: (services) => new SafeDsCoreClasses(services),
Annotations: (services) => new SafeDsAnnotations(services),
Classes: (services) => new SafeDsClasses(services),
},
helpers: {
NodeMapper: (services) => new SafeDsNodeMapper(services),
Expand Down
Loading

0 comments on commit 9b5287c

Please sign in to comment.