Skip to content
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Build package
run: npm run esbuild

- name: ESLint
run: npm run lint

- name: Test language server
run: npm run test:server

Expand Down
2 changes: 1 addition & 1 deletion client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {

let client: LanguageClient;

export function activate(context: ExtensionContext) {
export function activate(context: ExtensionContext): void {
// The server is implemented in node, point to packed module
const serverModule = context.asAbsolutePath(path.join('out', 'server.js'));
if (!fs.existsSync(serverModule)) {
Expand Down
11 changes: 7 additions & 4 deletions client/src/test/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ export let platformEol: string;
/**
* Activates the OpenModelica.modelica-language-server extension
*/
export async function activate(docUri: vscode.Uri) {
export async function activate(docUri: vscode.Uri): Promise<void> {
// The extensionId is `publisher.name` from package.json
const ext = vscode.extensions.getExtension('OpenModelica.modelica-language-server')!;
const ext = vscode.extensions.getExtension('OpenModelica.modelica-language-server');
if (!ext) {
throw new Error('Could not find OpenModelica.modelica-language-server extension');
}
await ext.activate();
try {
doc = await vscode.workspace.openTextDocument(docUri);
Expand All @@ -61,10 +64,10 @@ async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export const getDocPath = (p: string) => {
export const getDocPath = (p: string): string => {
return path.resolve(__dirname, '../../testFixture', p);
};

export const getDocUri = (p: string) => {
export const getDocUri = (p: string): vscode.Uri => {
return vscode.Uri.file(getDocPath(p));
};
2 changes: 1 addition & 1 deletion client/src/test/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async function main() {
extensionTestsPath,
launchArgs: [testFixturePath],
});
} catch (err) {
} catch (_err) {
console.error('Failed to run tests');
process.exit(1);
}
Expand Down
22 changes: 0 additions & 22 deletions client/src/test/symbolinformation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,9 @@ async function testSymbolInformation(
docUri,
);

//printDocumentSymbols(actualSymbolInformation);
assertDocumentSymbolsEqual(expectedDocumentSymbols, actualSymbolInformation);
}

function printDocumentSymbols(documentSymbols: vscode.DocumentSymbol[]) {
documentSymbols.forEach((symbol, index) => {
console.log(`Document Symbol ${index + 1}:`);
console.log(`Name: ${symbol.name}`);
console.log(`Kind: ${vscode.SymbolKind[symbol.kind]}`);
console.log(
`Range: ${symbol.range.start.line}:${symbol.range.start.character}, ${symbol.range.end.line}:${symbol.range.end.character}`,
);
console.log(
`SelectionRange: ${symbol.selectionRange.start.line}:${symbol.selectionRange.start.character}, ${symbol.selectionRange.end.line}:${symbol.selectionRange.end.character}`,
);
console.log('Children:');

if (symbol.children && symbol.children.length > 0) {
printDocumentSymbols(symbol.children);
}

console.log('---');
});
}

function assertDocumentSymbolsEqual(
expected: vscode.DocumentSymbol[],
actual: vscode.DocumentSymbol[],
Expand Down
1 change: 0 additions & 1 deletion client/tsconfig.tsbuildinfo

This file was deleted.

16 changes: 10 additions & 6 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ module.exports = [
'semi': [2, 'always'],
'no-undef': 0, // TypeScript's type checker handles this
'no-redeclare': 0, // @typescript-eslint/no-redeclare handles TS overloads
'no-unused-private-class-members': 0,
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/no-unused-expressions': 0,
'no-unused-private-class-members': 1,
'@typescript-eslint/no-unused-vars': [1, {
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_',
'caughtErrorsIgnorePattern': '^_',
}],
'@typescript-eslint/no-explicit-any': 1,
'@typescript-eslint/explicit-module-boundary-types': 1,
'@typescript-eslint/no-non-null-assertion': 1,
'@typescript-eslint/no-unused-expressions': 1,
},
},
];
2 changes: 1 addition & 1 deletion server/src/analysis/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/

import { ModelicaDocument } from '../project/document';
import { Parser, Node as SyntaxNode } from 'web-tree-sitter';
import { Node as SyntaxNode } from 'web-tree-sitter';

export type ReferenceKind = 'class' | 'variable';

Expand Down
6 changes: 3 additions & 3 deletions server/src/analysis/resolveReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*
*/

import { Parser, Node as SyntaxNode } from 'web-tree-sitter';
import { Node as SyntaxNode } from 'web-tree-sitter';
import * as fs from 'node:fs';
import * as path from 'node:path';

Expand Down Expand Up @@ -296,7 +296,7 @@ function findDeclarationInClass(
) as UnresolvedRelativeReference & { kind: 'class' };
}

const componentDef = namedElement[0].childForFieldName('componentClause')!;
const componentDef = TreeSitterUtil.requireFieldName(namedElement[0], 'componentClause');

// TODO: this handles named_elements but what if it's an import clause?
return new UnresolvedRelativeReference(
Expand Down Expand Up @@ -374,7 +374,7 @@ function resolveImportClause(
): ResolveImportClauseResult {
// imports are always relative according to the grammar
const importPath = TreeSitterUtil.getTypeSpecifier(
importClause.childForFieldName('name')!,
TreeSitterUtil.requireFieldName(importClause, 'name'),
).symbols;

// wildcard import: import a.b.*;
Expand Down
32 changes: 21 additions & 11 deletions server/src/analysis/test/resolveReference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import assert from 'node:assert/strict';
import path from 'node:path';
import { ModelicaProject, ModelicaLibrary, ModelicaDocument } from '../../project';
import { ModelicaProject, ModelicaLibrary } from '../../project';
import { initializeParser } from '../../parser';
import resolveReference from '../resolveReference';
import {
Expand Down Expand Up @@ -70,7 +70,8 @@ describe('resolveReference', () => {
resolvedDocument.tree.rootNode,
(node) =>
node.type === 'class_definition' && TreeSitterUtil.getIdentifier(node) === 'TestClass',
)!;
);
assert.ok(resolvedNode);
const resolvedSymbols = ['TestLibrary', 'TestPackage', 'TestClass'];

assert(
Expand All @@ -84,14 +85,16 @@ describe('resolveReference', () => {
const unresolved = new UnresolvedAbsoluteReference(['TestLibrary', 'Constants', 'e']);
const resolved = resolveReference(project, unresolved, 'declaration');

const resolvedDocument = (await project.getDocument(CONSTANTS_PATH))!;
const resolvedDocument = await project.getDocument(CONSTANTS_PATH);
assert.ok(resolvedDocument);

// Get the node declaring `e`
const resolvedNode = TreeSitterUtil.findFirst(
resolvedDocument.tree.rootNode,
(node) =>
node.type === 'component_clause' && TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'e',
)!;
);
assert.ok(resolvedNode);
const resolvedSymbols = ['TestLibrary', 'Constants', 'e'];

assert(
Expand All @@ -102,11 +105,13 @@ describe('resolveReference', () => {
});

it('should resolve relative references to locals', async () => {
const document = (await project.getDocument(TEST_CLASS_PATH))!;
const document = await project.getDocument(TEST_CLASS_PATH);
assert.ok(document);
const unresolvedNode = TreeSitterUtil.findFirst(
document.tree.rootNode,
(node) => node.startPosition.row === 7 && node.startPosition.column === 21,
)!;
);
assert.ok(unresolvedNode);
const unresolved = new UnresolvedRelativeReference(document, unresolvedNode, ['tau']);
const resolved = resolveReference(project, unresolved, 'declaration');

Expand All @@ -117,7 +122,8 @@ describe('resolveReference', () => {
(node) =>
node.type === 'component_clause' &&
TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'tau',
)!;
);
assert.ok(resolvedNode);

assert(
resolved?.equals(
Expand All @@ -134,24 +140,28 @@ describe('resolveReference', () => {
it('should resolve relative references to globals', async () => {
// input Real twoE = 2 * Constants.e;
// ^ 5:33
const unresolvedDocument = (await project.getDocument(TEST_CLASS_PATH))!;
const unresolvedDocument = await project.getDocument(TEST_CLASS_PATH);
assert.ok(unresolvedDocument);
const unresolvedNode = TreeSitterUtil.findFirst(
unresolvedDocument.tree.rootNode,
(node) => node.startPosition.row === 5 && node.startPosition.column === 33,
)!;
);
assert.ok(unresolvedNode);
const unresolved = new UnresolvedRelativeReference(unresolvedDocument, unresolvedNode, [
'Constants',
'e',
]);
const resolved = resolveReference(project, unresolved, 'declaration');

const resolvedDocument = (await project.getDocument(CONSTANTS_PATH))!;
const resolvedDocument = await project.getDocument(CONSTANTS_PATH);
assert.ok(resolvedDocument);
// Get the node declaring `e`
const resolvedNode = TreeSitterUtil.findFirst(
resolvedDocument.tree.rootNode,
(node) =>
node.type === 'component_clause' && TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'e',
)!;
);
assert.ok(resolvedNode);

assert(
resolved?.equals(
Expand Down
10 changes: 8 additions & 2 deletions server/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,10 @@ export default class Analyzer {
hoveredType,
documentOffset,
(node) => node.type === 'IDENT',
)!;
);
if (!startNode) {
return null;
}

return new UnresolvedRelativeReference(document, startNode, symbols, 'class');
}
Expand All @@ -297,7 +300,10 @@ export default class Analyzer {
hoveredComponentReference,
documentOffset,
(node) => node.type === 'IDENT',
)!;
);
if (!startNode) {
return null;
}

return new UnresolvedRelativeReference(document, startNode, symbols, 'variable');
}
Expand Down
21 changes: 16 additions & 5 deletions server/src/project/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import { TextDocument } from 'vscode-languageserver-textdocument';
import * as LSP from 'vscode-languageserver/node';
import { Parser, Tree, Point, Edit } from 'web-tree-sitter';
import { Tree, Edit } from 'web-tree-sitter';
import * as fs from 'node:fs/promises';
import * as TreeSitterUtil from '../util/tree-sitter';

Expand Down Expand Up @@ -85,8 +85,11 @@ export class ModelicaDocument implements TextDocument {
const document = TextDocument.create(uri, 'modelica', 0, content);

const tree = project.parser.parse(content);
if (!tree) {
throw new Error('parser returned no tree');
}

return new ModelicaDocument(project, library, document, tree!);
return new ModelicaDocument(project, library, document, tree);
} catch (err) {
throw new Error(
`Failed to load document at '${documentPath}': ${err instanceof Error ? err.message : err}`,
Expand All @@ -104,7 +107,11 @@ export class ModelicaDocument implements TextDocument {
public async update(text: string, range?: LSP.Range): Promise<void> {
if (range === undefined) {
TextDocument.update(this.#document, [{ text }], this.version + 1);
this.#tree = this.project.parser.parse(text)!;
const tree = this.project.parser.parse(text);
if (!tree) {
throw new Error(`Failed to parse updated document '${this.uri}'`);
}
this.#tree = tree;
return;
}

Expand All @@ -127,10 +134,14 @@ export class ModelicaDocument implements TextDocument {
}));

const fullText = this.getText();
this.#tree = this.project.parser.parse(
const tree = this.project.parser.parse(
(index: number) => fullText[index] ?? '',
this.#tree,
)!;
);
if (!tree) {
throw new Error(`Failed to parse updated document '${this.uri}'`);
}
this.#tree = tree;
}

public getText(range?: LSP.Range | undefined): string {
Expand Down
1 change: 0 additions & 1 deletion server/src/project/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
*
*/

import * as LSP from 'vscode-languageserver';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';

Expand Down
3 changes: 1 addition & 2 deletions server/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@

import { Parser } from 'web-tree-sitter';
import * as LSP from 'vscode-languageserver';
import url from 'node:url';
import path from 'node:path';

import { ModelicaLibrary } from './library';
Expand Down Expand Up @@ -65,7 +64,7 @@ export class ModelicaProject {
return this.#libraries;
}

public addLibrary(library: ModelicaLibrary) {
public addLibrary(library: ModelicaLibrary): void {
this.#libraries.push(library);
}

Expand Down
12 changes: 8 additions & 4 deletions server/src/project/test/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ describe('ModelicaDocument', () => {

it('can update the entire document', () => {
const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT);
const tree = project.parser.parse(TEST_PACKAGE_CONTENT)!;
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
assert.ok(tree);
const document = new ModelicaDocument(project, library, textDocument, tree);
document.update(UPDATED_TEST_PACKAGE_CONTENT);

Expand All @@ -84,7 +85,8 @@ describe('ModelicaDocument', () => {

it('can update incrementally', () => {
const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT);
const tree = project.parser.parse(TEST_PACKAGE_CONTENT)!;
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
assert.ok(tree);
const document = new ModelicaDocument(project, library, textDocument, tree);
document.update(
'1.0.1',
Expand Down Expand Up @@ -124,15 +126,17 @@ describe('ModelicaDocument', () => {

it('a file with no `within` clause has the correct package path', () => {
const textDocument = createTextDocument('./package.mo', TEST_PACKAGE_CONTENT);
const tree = project.parser.parse(TEST_PACKAGE_CONTENT)!;
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
assert.ok(tree);
const document = new ModelicaDocument(project, library, textDocument, tree);

assert.deepEqual(document.within, []);
});

it('a file with a `within` clause has the correct package path', () => {
const textDocument = createTextDocument('./Foo/Bar/Frobnicator.mo', TEST_CLASS_CONTENT);
const tree = project.parser.parse(TEST_CLASS_CONTENT)!;
const tree = project.parser.parse(TEST_CLASS_CONTENT);
assert.ok(tree);
const document = new ModelicaDocument(project, library, textDocument, tree);

assert.deepEqual(document.within, ['TestPackage', 'Foo', 'Bar']);
Expand Down
3 changes: 2 additions & 1 deletion server/src/project/test/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ describe('ModelicaProject', () => {
});

it('documents can be updated', async () => {
const document = (await project.getDocument(TEST_PACKAGE_PATH))!;
const document = await project.getDocument(TEST_PACKAGE_PATH);
assert.ok(document);
assert.equal(
document.getText().replace(/\r\n/g, '\n'),
TEST_PACKAGE_CONTENT.replace(/\r\n/g, '\n'),
Expand Down
Loading
Loading