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

Import from anywhere #604

Merged
merged 20 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4831d0c
Use JayveeService instead of Langium specific one in utils
georg-schwarz Jul 26, 2024
f60e4e7
Add method findUnresolvedImportURIs to JayveeImportResolver
georg-schwarz Jul 26, 2024
11849b5
Add hook on workspace initialization to load unresolved documents out…
georg-schwarz Jul 26, 2024
a79bbd3
Initialize the workspace in the VSCode extension
georg-schwarz Jul 26, 2024
f619fe4
Dynamic file loading from fs as string instead from URI to allow brow…
georg-schwarz Jul 26, 2024
641d3be
Use FileSystemProvider instead of node:fs
georg-schwarz Jul 26, 2024
b9abbaa
Add launch config for electric vehicle example
georg-schwarz Jul 26, 2024
675c795
Add hooks before initializing the workspace
georg-schwarz Jul 26, 2024
1da7dc2
Use mutex to prevent adding documents multiple times
georg-schwarz Jul 26, 2024
835f9ca
Only load documents with correct file extension
georg-schwarz Jul 26, 2024
9a93a06
Fix error message when import cannot be resolved
georg-schwarz Jul 26, 2024
2803955
Refactor check on correct file extension before loading
georg-schwarz Jul 26, 2024
d942134
Remove mutex due to deadlock
georg-schwarz Jul 26, 2024
4ce82e2
Add tests for the dynamic import
georg-schwarz Jul 26, 2024
74a5ae0
Place more narrow mutex lock around adding new documents
georg-schwarz Jul 29, 2024
25274d6
Rename std-lib directory to workspace
georg-schwarz Jul 29, 2024
86a66f7
Extract dynamic file import to own file and initialize while creating…
georg-schwarz Jul 29, 2024
4d4b10c
Add license text
georg-schwarz Jul 29, 2024
add26e4
Fix copyright year
georg-schwarz Jul 30, 2024
77c608c
Fix typo in test file name
georg-schwarz Jul 30, 2024
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
6 changes: 6 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
"request": "launch",
"type": "node-terminal"
},
{
"name": "Run Electric Vehicles Example",
"command": "npm run example:vehicles",
"request": "launch",
"type": "node-terminal"
},
{
"name": "Run Gas Reserve Example",
"command": "npm run example:gas",
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/docs/dev/04-guides/05-standard-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ We use code generation to transform these `.jv` files into TypeScript files that

### 2. Builtin libraries

The solution we chose to implement the standard library mechanism is close to the [built-in library tutorial](https://langium.org/guides/builtin-library/) by Langium. The following components are of interest:
The solution we chose to implement the standard library mechanism is close to the [built-in library tutorial](https://langium.org/guides/workspace/) by Langium. The following components are of interest:

- [JayveeWorkspaceManager](https://github.com/jvalue/jayvee/tree/main/libs/language-server/src/lib/builtin-library/jayvee-workspace-manager.ts) in the `language-server` that registers all libraries with the langium framework.
- [JayveeWorkspaceManager](https://github.com/jvalue/jayvee/tree/main/libs/language-server/src/lib/workspace/jayvee-workspace-manager.ts) in the `language-server` that registers all libraries with the langium framework.
- [StandardLibraryFileSystemProvider](https://github.com/jvalue/jayvee/tree/main/apps/vs-code-extension/src/standard-library-file-system-provider.ts) in the `vs-code-extension` that registers all libraries with the vscode plugin framework.
1 change: 0 additions & 1 deletion apps/vs-code-extension/src/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ const { shared } = createJayveeServices({
...NodeFileSystem,
});

// Start the language server with the shared services
startLanguageServer(shared);
11 changes: 7 additions & 4 deletions libs/interpreter-lib/src/parsing-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import * as fs from 'node:fs';
import path from 'node:path';

import { type Logger } from '@jvalue/jayvee-execution';
import { initializeWorkspace } from '@jvalue/jayvee-language-server';
import {
type JayveeServices,
initializeWorkspace,
} from '@jvalue/jayvee-language-server';
import { type AstNode, type LangiumDocument } from 'langium';
import { type LangiumServices } from 'langium/lsp';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
Expand Down Expand Up @@ -58,7 +61,7 @@ export async function extractDocumentFromFile(
*/
export async function extractDocumentFromString(
modelString: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<LangiumDocument> {
const document = services.shared.workspace.LangiumDocumentFactory.fromString(
Expand Down Expand Up @@ -104,7 +107,7 @@ export async function validateDocument(

export async function extractAstNodeFromFile<T extends AstNode>(
filePath: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<T> {
return (await extractDocumentFromFile(filePath, services, logger)).parseResult
Expand All @@ -113,7 +116,7 @@ export async function extractAstNodeFromFile<T extends AstNode>(

export async function extractAstNodeFromString<T extends AstNode>(
modelString: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<T> {
return (await extractDocumentFromString(modelString, services, logger))
Expand Down
2 changes: 1 addition & 1 deletion libs/language-server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

# files generated by Langium
src/lib/ast/generated
src/lib/builtin-library/generated
src/lib/workspace/generated
2 changes: 1 addition & 1 deletion libs/language-server/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only

export * from './ast';
export * from './builtin-library';
export * from './workspace';
export * from './docs';
export * from './services';
export * from './util';
Expand Down
5 changes: 4 additions & 1 deletion libs/language-server/src/lib/jayvee-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
} from './ast/generated/module';
import { ValueTypeProvider } from './ast/wrappers/value-type/primitive/primitive-value-type-provider';
import { WrapperFactoryProvider } from './ast/wrappers/wrapper-factory-provider';
import { JayveeWorkspaceManager } from './builtin-library/jayvee-workspace-manager';
import { JayveeValueConverter } from './jayvee-value-converter';
import {
JayveeCodeActionProvider,
Expand All @@ -37,6 +36,8 @@ import {
import { JayveeImportResolver } from './services/import-resolver';
import { RuntimeParameterProvider } from './services/runtime-parameter-provider';
import { JayveeValidationRegistry } from './validation/validation-registry';
import { addDynamicFileImport } from './workspace';
import { JayveeWorkspaceManager } from './workspace/jayvee-workspace-manager';

/**
* Declaration of custom services for the Jayvee language.
Expand Down Expand Up @@ -150,5 +151,7 @@ export function createJayveeServices(context: DefaultSharedModuleContext): {
);
shared.ServiceRegistry.register(Jayvee);

addDynamicFileImport(Jayvee);

return { shared, Jayvee };
}
90 changes: 90 additions & 0 deletions libs/language-server/src/lib/lsp/import-dynamic-reference.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import path from 'node:path';

import { NodeFileSystem } from 'langium/node';

import {
type JayveeServices,
createJayveeServices,
isJayveeModel,
} from '../../lib';
import {
expectNoParserAndLexerErrors,
parseTestFileInWorkingDir,
} from '../../test';
import { type JayveeModel } from '../ast';

describe('References to imported elements outside of the working directory', () => {
const WORKING_DIR = path.resolve(
__dirname,
'../../test/assets/import-dynamic-reference/models', // use the "deep" directory as working dir to avoid loading the "higher" dir
);
let services: JayveeServices;

async function parseModel(
relativeTestFilePath: string,
): Promise<JayveeModel> {
const document = await parseTestFileInWorkingDir(
WORKING_DIR,
relativeTestFilePath,
services,
);
expectNoParserAndLexerErrors(document);

const parsedModel = document.parseResult.value;
assert(isJayveeModel(parsedModel), 'Test file is not valid Jayvee model');
return parsedModel;
}

beforeEach(() => {
// Create language services
services = createJayveeServices(NodeFileSystem).Jayvee;
});

const validCases: [string, string][] = [
// [test name, test file path]
[
'should resolve reference to imported element',
'valid-import-from-outside-workdir.jv',
],
];
test.each(validCases)('%s', async (_, relativeTestFilePath) => {
const model = await parseModel(relativeTestFilePath);

expect(model.pipelines.length).toEqual(1); // file contains one pipeline
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const blocks = model.pipelines[0]!.blocks;
expect(blocks.length).toEqual(1); // pipeline contains one block
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const reference = blocks[0]!.type; // of an imported block type

expect(reference.ref).toBeDefined();
});

const invalidCases: [string, string][] = [
// [test name, test file path]
[
'should not resolve reference to file with no jv extension',
'invalid-import-wrong-file-type.jv',
],
[
'should not resolve reference to file that does not exist',
'invalid-import-not-existing-file.jv',
],
];
test.each(invalidCases)('%s', async (_, relativeTestFilePath) => {
const model = await parseModel(relativeTestFilePath);

expect(model.pipelines.length).toEqual(1); // file contains one pipeline
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const blocks = model.pipelines[0]!.blocks;
expect(blocks.length).toEqual(1); // pipeline contains one block
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const reference = blocks[0]!.type; // of an imported block type

expect(reference.ref).toBeUndefined();
});
});
21 changes: 20 additions & 1 deletion libs/language-server/src/lib/services/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
isExportableElement,
isJayveeModel,
} from '../ast/generated/ast';
import { getStdLib } from '../builtin-library/stdlib';
import { type JayveeServices } from '../jayvee-module';
import { getStdLib } from '../workspace/stdlib';

export interface ImportDetails {
element: ExportableElement;
Expand Down Expand Up @@ -52,6 +52,25 @@ export class JayveeImportResolver {
this.availableElementsPerDocumentCache = new DocumentCache(services.shared);
}

/**
* Finds all import URIs that could not be resolved.
*/
findUnresolvedImportURIs(model: JayveeModel): URI[] {
const unresolvedURIs: URI[] = [];
for (const importDefinition of model.imports) {
const uri = this.resolveImportUri(importDefinition);
if (uri === undefined) {
continue;
}

const isDocumentResolved = this.documents.getDocument(uri) !== undefined;
if (!isDocumentResolved) {
unresolvedURIs.push(uri);
}
}
return unresolvedURIs;
}

resolveImport(importDefinition: ImportDefinition): JayveeModel | undefined {
const resolvedDocument = this.resolveImportedDocument(importDefinition);
if (resolvedDocument === undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ describe('Validation of ImportDefinition', () => {
expect(validationAcceptorMock).toHaveBeenNthCalledWith(
1,
'error',
'Import from "not-existing-imported-file-deeper.jv" could be resolved. Check if the file exists in the given location.',
'Import from "not-existing-imported-file-deeper.jv" could not be resolved. Check if the file exists in the given location.',
expect.any(Object),
);
expect(validationAcceptorMock).toHaveBeenNthCalledWith(
2,
'error',
'Import from "./not-existing-imported-file-deeper.jv" could be resolved. Check if the file exists in the given location.',
'Import from "./not-existing-imported-file-deeper.jv" could not be resolved. Check if the file exists in the given location.',
expect.any(Object),
);
});
Expand All @@ -121,13 +121,13 @@ describe('Validation of ImportDefinition', () => {
expect(validationAcceptorMock).toHaveBeenNthCalledWith(
1,
'error',
'Import from "existing-imported-file.njv" could be resolved. Check if the file exists in the given location.',
'Import from "existing-imported-file.njv" could not be resolved. Check if the file exists in the given location.',
expect.any(Object),
);
expect(validationAcceptorMock).toHaveBeenNthCalledWith(
2,
'error',
'Import from "./existing-imported-file.njv" could be resolved. Check if the file exists in the given location.',
'Import from "./existing-imported-file.njv" could not be resolved. Check if the file exists in the given location.',
expect.any(Object),
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function checkPathExists(
if (resolvedImport === undefined) {
props.validationContext.accept(
'error',
`Import from "${importDefinition.path}" could be resolved. Check if the file exists in the given location.`,
`Import from "${importDefinition.path}" could not be resolved. Check if the file exists in the given location.`,
{
node: importDefinition,
property: 'path',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { NodeFileSystem } from 'langium/node';

import { validationHelper } from '../../test/langium-utils';
import { getAllBuiltinConstraintTypes } from '../ast';
import { initializeWorkspace } from '../builtin-library';
import { createJayveeServices } from '../jayvee-module';
import { initializeWorkspace } from '../workspace';

describe('Validation of docs examples of ConstraintTypes', () => {
it('should have no validation errors', async () => {
Expand Down
74 changes: 74 additions & 0 deletions libs/language-server/src/lib/workspace/dynamic-file-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import { DocumentState } from 'langium';
import { type URI } from 'vscode-uri';

import { type JayveeModel, isJayveeModel } from '../ast';
import { type JayveeServices } from '../jayvee-module';

export function addDynamicFileImport(services: JayveeServices): void {
const documentBuilder = services.shared.workspace.DocumentBuilder;
const importResolver = services.ImportResolver;

documentBuilder.onBuildPhase(DocumentState.IndexedContent, async (docs) => {
for (const doc of docs) {
const model = doc.parseResult.value;
if (!isJayveeModel(model)) {
return;
}
const importURIs = importResolver.findUnresolvedImportURIs(model);

for (const importURI of importURIs) {
await loadDocumentFromFs(importURI, services);
}
}
});
}

async function loadDocumentFromFs(
importURI: URI,
services: JayveeServices,
): Promise<void> {
const allowedFileExtensions = services.shared.ServiceRegistry.all.flatMap(
(e) => e.LanguageMetaData.fileExtensions,
);
const hasAllowedFileExtension = allowedFileExtensions.some((ext) =>
importURI.fsPath.endsWith(ext),
);
if (!hasAllowedFileExtension) {
return;
}

const langiumDocuments = services.shared.workspace.LangiumDocuments;
const documentBuilder = services.shared.workspace.DocumentBuilder;
const documentFactory = services.shared.workspace.LangiumDocumentFactory;

const file = await loadFileFromUri(importURI, services);
if (file === undefined) {
return;
}

const document = documentFactory.fromString<JayveeModel>(file, importURI);
await documentBuilder.build([document], { validation: true });

await services.shared.workspace.WorkspaceLock.write(() => {
if (!langiumDocuments.hasDocument(document.uri)) {
langiumDocuments.addDocument(document);
}
});
}

async function loadFileFromUri(
uri: URI,
services: JayveeServices,
): Promise<string | undefined> {
const fileSystemProvider = services.shared.workspace.FileSystemProvider;

try {
return await fileSystemProvider.readFile(uri);
} catch (e) {
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
// SPDX-License-Identifier: AGPL-3.0-only

export { getStdLib } from './stdlib';
export { addDynamicFileImport } from './dynamic-file-import';
export * from './jayvee-workspace-manager';
Loading
Loading