From 971d64756d9287d5c57d80c696251c42e168326e Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:20:11 +0200 Subject: [PATCH 1/6] Enable no-unused-vars lint rule and fix violations Turn on @typescript-eslint/no-unused-vars (warn) with underscore-ignore patterns for args, vars, and caught errors, so intentionally-unused identifiers can be prefixed with `_`. Fix the resulting warnings: - Remove unused imports (LSP, Parser, Point, url, logger, ModelicaDocument) - Remove unused GLOBAL_DECLARATION_LEAF_NODE_TYPES constant - Prefix unused caught error with `_` - Drop unused parser binding in server test Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test.yml | 3 +++ client/src/test/runTest.ts | 2 +- client/src/test/symbolinformation.test.ts | 22 ------------------- eslint.config.js | 6 ++++- server/src/analysis/reference.ts | 2 +- server/src/analysis/resolveReference.ts | 2 +- .../analysis/test/resolveReference.test.ts | 2 +- server/src/project/document.ts | 2 +- server/src/project/library.ts | 1 - server/src/project/project.ts | 1 - server/src/test/server.test.ts | 2 +- server/src/util/declarations.ts | 3 --- server/src/util/tree-sitter.ts | 3 +-- 13 files changed, 15 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b826bf6..2e7aa11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/client/src/test/runTest.ts b/client/src/test/runTest.ts index eda2745..a466f28 100644 --- a/client/src/test/runTest.ts +++ b/client/src/test/runTest.ts @@ -76,7 +76,7 @@ async function main() { extensionTestsPath, launchArgs: [testFixturePath], }); - } catch (err) { + } catch (_err) { console.error('Failed to run tests'); process.exit(1); } diff --git a/client/src/test/symbolinformation.test.ts b/client/src/test/symbolinformation.test.ts index c2ddfb6..8a7adfd 100644 --- a/client/src/test/symbolinformation.test.ts +++ b/client/src/test/symbolinformation.test.ts @@ -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[], diff --git a/eslint.config.js b/eslint.config.js index 1bd927a..a83adf8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,7 +42,11 @@ module.exports = [ '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-unused-vars': [1, { + 'argsIgnorePattern': '^_', + 'varsIgnorePattern': '^_', + 'caughtErrorsIgnorePattern': '^_', + }], '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-non-null-assertion': 0, diff --git a/server/src/analysis/reference.ts b/server/src/analysis/reference.ts index e02a4ef..cc13252 100644 --- a/server/src/analysis/reference.ts +++ b/server/src/analysis/reference.ts @@ -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'; diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index d51e4a4..5e25c5d 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -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'; diff --git a/server/src/analysis/test/resolveReference.test.ts b/server/src/analysis/test/resolveReference.test.ts index 31ea6cc..c099ebb 100644 --- a/server/src/analysis/test/resolveReference.test.ts +++ b/server/src/analysis/test/resolveReference.test.ts @@ -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 { diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 69a9175..abe3268 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -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'; diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 307bf77..061521a 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -33,7 +33,6 @@ * */ -import * as LSP from 'vscode-languageserver'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 6c2d033..9f412cd 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -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'; diff --git a/server/src/test/server.test.ts b/server/src/test/server.test.ts index 5c7b050..2f16ce4 100644 --- a/server/src/test/server.test.ts +++ b/server/src/test/server.test.ts @@ -49,7 +49,7 @@ const parsedModelicaTestString = describe('Modelica tree-sitter parser', () => { it('Initialize parser', async () => { - const parser = await initializeParser(); + await initializeParser(); }); it('Parse string', async () => { diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index 04b68a8..7eaafb6 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -42,15 +42,12 @@ import * as LSP from 'vscode-languageserver/node'; import { Tree, Node as SyntaxNode } from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; -import { logger } from './logger'; const isEmpty = (data: string): boolean => typeof data === 'string' && data.trim().length == 0; export type GlobalDeclarations = { [word: string]: LSP.SymbolInformation }; export type Declarations = { [word: string]: LSP.SymbolInformation[] }; -const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set(['if_statement', 'function_definition']); - /** * Returns all declarations (functions or variables) from a given tree. * diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 6ab0145..318ffeb 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -39,10 +39,9 @@ * ----------------------------------------------------------------------------- */ -import { Parser, Node as SyntaxNode, Point } from 'web-tree-sitter'; +import { Node as SyntaxNode, Point } from 'web-tree-sitter'; import * as LSP from 'vscode-languageserver/node'; -import { logger } from './logger'; import { TextDocument } from 'vscode-languageserver-textdocument'; /** From e4fbfcbb37eb6090d9740d64600773c8c56bae21 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:25:21 +0200 Subject: [PATCH 2/6] Enable no-explicit-any lint rule and remove any from logger Turn on @typescript-eslint/no-explicit-any (warn). Replace `any[]` with `unknown[]` in Logger.log and the debug/info/ warn/error helpers. The logger already narrows values at runtime (instanceof Error, typeof checks) and joins the rest with a string, so unknown is both sufficient and safer. Co-Authored-By: Claude Opus 4.8 --- eslint.config.js | 2 +- server/src/util/logger.ts | 10 +++++----- server/tsconfig.tsbuildinfo | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index a83adf8..b843367 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,7 +47,7 @@ module.exports = [ 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_', }], - '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-explicit-any': 1, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-non-null-assertion': 0, '@typescript-eslint/no-unused-expressions': 0, diff --git a/server/src/util/logger.ts b/server/src/util/logger.ts index 4d0dbc3..a9bcc4c 100644 --- a/server/src/util/logger.ts +++ b/server/src/util/logger.ts @@ -111,7 +111,7 @@ export class Logger { [LSP.MessageType.Debug]: console.debug, }; - public log(severity: LSP.MessageType, messageObjects: any[]) { + public log(severity: LSP.MessageType, messageObjects: unknown[]) { const logLevelString = _options.logLevel ?? getLogLevelFromEnvironment(); const logLevel = LOG_LEVELS_TO_MESSAGE_TYPES[logLevelString]; if (logLevel < severity) { @@ -150,16 +150,16 @@ export class Logger { } } - public debug(message: string, ...additionalArgs: any[]) { + public debug(message: string, ...additionalArgs: unknown[]) { this.log(LSP.MessageType.Debug, [message, ...additionalArgs]); } - public info(message: string, ...additionalArgs: any[]) { + public info(message: string, ...additionalArgs: unknown[]) { this.log(LSP.MessageType.Info, [message, ...additionalArgs]); } - public warn(message: string, ...additionalArgs: any[]) { + public warn(message: string, ...additionalArgs: unknown[]) { this.log(LSP.MessageType.Warning, [message, ...additionalArgs]); } - public error(message: string, ...additionalArgs: any[]) { + public error(message: string, ...additionalArgs: unknown[]) { this.log(LSP.MessageType.Error, [message, ...additionalArgs]); } } diff --git a/server/tsconfig.tsbuildinfo b/server/tsconfig.tsbuildinfo index 7fc0170..a5ecbdf 100644 --- a/server/tsconfig.tsbuildinfo +++ b/server/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/analyzer.ts","./src/parser.ts","./src/server.ts","./src/analysis/reference.ts","./src/analysis/resolveReference.ts","./src/analysis/test/resolveReference.test.ts","./src/project/document.ts","./src/project/index.ts","./src/project/library.ts","./src/project/project.ts","./src/project/test/document.test.ts","./src/project/test/project.test.ts","./src/test/server.test.ts","./src/util/declarations.ts","./src/util/index.ts","./src/util/logger.ts","./src/util/tree-sitter.ts","./src/util/test/declarations.test.ts","./src/util/test/util.test.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/analyzer.ts","./src/parser.ts","./src/server.ts","./src/analysis/reference.ts","./src/analysis/resolveReference.ts","./src/analysis/test/resolveReference.test.ts","./src/project/document.ts","./src/project/index.ts","./src/project/library.ts","./src/project/project.ts","./src/project/test/document.test.ts","./src/project/test/project.test.ts","./src/test/analyzer.test.ts","./src/test/server.test.ts","./src/util/declarations.ts","./src/util/index.ts","./src/util/logger.ts","./src/util/tree-sitter.ts","./src/util/test/declarations.test.ts","./src/util/test/util.test.ts"],"version":"6.0.3"} \ No newline at end of file From 6e911eb6f16ae20bf72b4a8de19132c00fc8c028 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:28:27 +0200 Subject: [PATCH 3/6] Enable explicit-module-boundary-types and add return types Turn on @typescript-eslint/explicit-module-boundary-types (warn) and annotate the return types of all exported/public boundary functions in the client and server. Co-Authored-By: Claude Opus 4.8 --- client/src/extension.ts | 2 +- client/src/test/helper.ts | 6 +++--- eslint.config.js | 2 +- server/src/project/project.ts | 2 +- server/src/util/logger.ts | 12 ++++++------ server/src/util/tree-sitter.ts | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/src/extension.ts b/client/src/extension.ts index 2b1dcdd..3443454 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -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)) { diff --git a/client/src/test/helper.ts b/client/src/test/helper.ts index 70a95b7..dfb58f0 100644 --- a/client/src/test/helper.ts +++ b/client/src/test/helper.ts @@ -44,7 +44,7 @@ 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 { // The extensionId is `publisher.name` from package.json const ext = vscode.extensions.getExtension('OpenModelica.modelica-language-server')!; await ext.activate(); @@ -61,10 +61,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)); }; diff --git a/eslint.config.js b/eslint.config.js index b843367..3b3b5cc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -48,7 +48,7 @@ module.exports = [ 'caughtErrorsIgnorePattern': '^_', }], '@typescript-eslint/no-explicit-any': 1, - '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/explicit-module-boundary-types': 1, '@typescript-eslint/no-non-null-assertion': 0, '@typescript-eslint/no-unused-expressions': 0, }, diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 9f412cd..901f587 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -64,7 +64,7 @@ export class ModelicaProject { return this.#libraries; } - public addLibrary(library: ModelicaLibrary) { + public addLibrary(library: ModelicaLibrary): void { this.#libraries.push(library); } diff --git a/server/src/util/logger.ts b/server/src/util/logger.ts index a9bcc4c..31abd89 100644 --- a/server/src/util/logger.ts +++ b/server/src/util/logger.ts @@ -84,7 +84,7 @@ let _options: LoggerOptions = {}; /** * Sets the logger options. Should be done at startup. */ -export function setLoggerOptions(options: LoggerOptions) { +export function setLoggerOptions(options: LoggerOptions): void { _options = options; } @@ -111,7 +111,7 @@ export class Logger { [LSP.MessageType.Debug]: console.debug, }; - public log(severity: LSP.MessageType, messageObjects: unknown[]) { + public log(severity: LSP.MessageType, messageObjects: unknown[]): void { const logLevelString = _options.logLevel ?? getLogLevelFromEnvironment(); const logLevel = LOG_LEVELS_TO_MESSAGE_TYPES[logLevelString]; if (logLevel < severity) { @@ -150,16 +150,16 @@ export class Logger { } } - public debug(message: string, ...additionalArgs: unknown[]) { + public debug(message: string, ...additionalArgs: unknown[]): void { this.log(LSP.MessageType.Debug, [message, ...additionalArgs]); } - public info(message: string, ...additionalArgs: unknown[]) { + public info(message: string, ...additionalArgs: unknown[]): void { this.log(LSP.MessageType.Info, [message, ...additionalArgs]); } - public warn(message: string, ...additionalArgs: unknown[]) { + public warn(message: string, ...additionalArgs: unknown[]): void { this.log(LSP.MessageType.Warning, [message, ...additionalArgs]); } - public error(message: string, ...additionalArgs: unknown[]) { + public error(message: string, ...additionalArgs: unknown[]): void { this.log(LSP.MessageType.Error, [message, ...additionalArgs]); } } diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 318ffeb..3d83a45 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -50,7 +50,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; * @param node The node to start iterating from * @param callback The callback to call for each node. Return false to stop following children. */ -export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | boolean) { +export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | boolean): void { const followChildren = callback(node) !== false; if (followChildren && node.children.length) { node.children.forEach((n) => forEach(n, callback)); From 00775b9e973572dceb84615868e2d2daa7fbbe42 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:54:56 +0200 Subject: [PATCH 4/6] Enable no-non-null-assertion and remove non-null assertions Turn on @typescript-eslint/no-non-null-assertion (warn) and replace all non-null assertions with explicit handling: - Add a requireFieldName() helper in tree-sitter that throws a descriptive error for grammar-guaranteed fields, and use it in tree-sitter and resolveReference - Return null from Analyzer.getReferenceAt when no IDENT node is found, matching the existing fallback path - Check parser.parse() results for null in ModelicaDocument and throw on failure - Guard the extension lookup in the test helper - Replace assertions in tests with assert.ok() checks Co-Authored-By: Claude Opus 4.8 --- client/src/test/helper.ts | 5 ++- eslint.config.js | 2 +- server/src/analysis/resolveReference.ts | 4 +- .../analysis/test/resolveReference.test.ts | 30 +++++++++----- server/src/analyzer.ts | 10 ++++- server/src/project/document.ts | 19 +++++++-- server/src/project/test/document.test.ts | 12 ++++-- server/src/project/test/project.test.ts | 3 +- server/src/util/test/declarations.test.ts | 7 ++-- server/src/util/test/util.test.ts | 7 ++-- server/src/util/tree-sitter.ts | 39 ++++++++++++++----- 11 files changed, 97 insertions(+), 41 deletions(-) diff --git a/client/src/test/helper.ts b/client/src/test/helper.ts index dfb58f0..2b0a785 100644 --- a/client/src/test/helper.ts +++ b/client/src/test/helper.ts @@ -46,7 +46,10 @@ export let platformEol: string; */ export async function activate(docUri: vscode.Uri): Promise { // 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); diff --git a/eslint.config.js b/eslint.config.js index 3b3b5cc..e57e565 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -49,7 +49,7 @@ module.exports = [ }], '@typescript-eslint/no-explicit-any': 1, '@typescript-eslint/explicit-module-boundary-types': 1, - '@typescript-eslint/no-non-null-assertion': 0, + '@typescript-eslint/no-non-null-assertion': 1, '@typescript-eslint/no-unused-expressions': 0, }, }, diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 5e25c5d..4156414 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -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( @@ -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.*; diff --git a/server/src/analysis/test/resolveReference.test.ts b/server/src/analysis/test/resolveReference.test.ts index c099ebb..5572be5 100644 --- a/server/src/analysis/test/resolveReference.test.ts +++ b/server/src/analysis/test/resolveReference.test.ts @@ -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( @@ -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( @@ -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'); @@ -117,7 +122,8 @@ describe('resolveReference', () => { (node) => node.type === 'component_clause' && TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'tau', - )!; + ); + assert.ok(resolvedNode); assert( resolved?.equals( @@ -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( diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index ef9086a..180f87c 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -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'); } @@ -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'); } diff --git a/server/src/project/document.ts b/server/src/project/document.ts index abe3268..34b07f7 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -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}`, @@ -104,7 +107,11 @@ export class ModelicaDocument implements TextDocument { public async update(text: string, range?: LSP.Range): Promise { 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; } @@ -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 { diff --git a/server/src/project/test/document.test.ts b/server/src/project/test/document.test.ts index a341232..034358d 100644 --- a/server/src/project/test/document.test.ts +++ b/server/src/project/test/document.test.ts @@ -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); @@ -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', @@ -124,7 +126,8 @@ 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, []); @@ -132,7 +135,8 @@ describe('ModelicaDocument', () => { 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']); diff --git a/server/src/project/test/project.test.ts b/server/src/project/test/project.test.ts index b02fb99..eb6e9b2 100644 --- a/server/src/project/test/project.test.ts +++ b/server/src/project/test/project.test.ts @@ -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'), diff --git a/server/src/util/test/declarations.test.ts b/server/src/util/test/declarations.test.ts index a6e4d3e..3454643 100644 --- a/server/src/util/test/declarations.test.ts +++ b/server/src/util/test/declarations.test.ts @@ -58,9 +58,10 @@ describe('nodeToSymbolInformation', () => { const tree = parser.parse('type Temperature = Real(unit = "K ");'); assert.ok(tree, 'parser.parse returned null'); - const classNode = tree.rootNode - .childForFieldName('storedDefinitions')! - .childForFieldName('classDefinition')!; + const storedDefinitions = tree.rootNode.childForFieldName('storedDefinitions'); + assert.ok(storedDefinitions); + const classNode = storedDefinitions.childForFieldName('classDefinition'); + assert.ok(classNode); const symbol = nodeToSymbolInformation(classNode, 'file.mo'); assert.equal(symbol?.name, 'Temperature'); diff --git a/server/src/util/test/util.test.ts b/server/src/util/test/util.test.ts index fc2f043..e29d671 100644 --- a/server/src/util/test/util.test.ts +++ b/server/src/util/test/util.test.ts @@ -43,9 +43,10 @@ describe('getIdentifier', () => { const parser = await initializeParser(); const tree = parser.parse('type Temperature = Real(unit = "K ");'); assert.ok(tree, 'parser.parse returned null'); - const classNode = tree.rootNode - .childForFieldName('storedDefinitions')! - .childForFieldName('classDefinition')!; + const storedDefinitions = tree.rootNode.childForFieldName('storedDefinitions'); + assert.ok(storedDefinitions); + const classNode = storedDefinitions.childForFieldName('classDefinition'); + assert.ok(classNode); const name = TreeSitterUtil.getIdentifier(classNode); assert.equal(name, 'Temperature'); diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 3d83a45..25d233c 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -183,6 +183,25 @@ export function getIdentifier(start: SyntaxNode): string | undefined { return node?.text; } +/** + * Returns the child of `node` for the given `fieldName`. + * + * Use this for fields that the grammar guarantees to be present: it throws a + * descriptive error instead of returning `null`, so a grammar mismatch fails + * loudly rather than as an opaque `undefined` access further down. + * + * @param node The node to read the field from. + * @param fieldName The name of the field. + * @returns The child node for the field. + */ +export function requireFieldName(node: SyntaxNode, fieldName: string): SyntaxNode { + const child = node.childForFieldName(fieldName); + if (!child) { + throw new Error(`Expected node '${node.type}' to have a '${fieldName}' field`); + } + return child; +} + /** * Returns the identifier(s) declared by the given node, or `[]` if no * identifiers are declared. @@ -209,7 +228,7 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { case 'short_class_specifier': case 'enumeration_literal': case 'for_index': - return [node.childForFieldName('identifier')!.text]; + return [requireFieldName(node, 'identifier').text]; case 'stored_definitions': case 'component_list': case 'enum_list': @@ -219,21 +238,21 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { case 'for_indices': return node.namedChildren.flatMap(getDeclaredIdentifiers); case 'component_clause': - return getDeclaredIdentifiers(node.childForFieldName('componentDeclarations')!); + return getDeclaredIdentifiers(requireFieldName(node, 'componentDeclarations')); case 'component_declaration': - return getDeclaredIdentifiers(node.childForFieldName('declaration')!); + return getDeclaredIdentifiers(requireFieldName(node, 'declaration')); case 'component_redeclaration': - return getDeclaredIdentifiers(node.childForFieldName('componentClause')!); + return getDeclaredIdentifiers(requireFieldName(node, 'componentClause')); case 'stored_definition': - return getDeclaredIdentifiers(node.childForFieldName('classDefinition')!); + return getDeclaredIdentifiers(requireFieldName(node, 'classDefinition')); case 'class_definition': - return getDeclaredIdentifiers(node.childForFieldName('classSpecifier')!); + return getDeclaredIdentifiers(requireFieldName(node, 'classSpecifier')); case 'for_equation': case 'for_statement': - return getDeclaredIdentifiers(node.childForFieldName('indices')!); + return getDeclaredIdentifiers(requireFieldName(node, 'indices')); case 'named_element': { const definition = - node.childForFieldName('classDefinition') ?? node.childForFieldName('componentClause')!; + node.childForFieldName('classDefinition') ?? requireFieldName(node, 'componentClause'); return getDeclaredIdentifiers(definition); } default: @@ -259,7 +278,7 @@ export function getTypeSpecifier(node: SyntaxNode): TypeSpecifier { switch (node.type) { case 'type_specifier': { const isGlobal = node.childForFieldName('global') !== null; - const name = node.childForFieldName('name')!; + const name = requireFieldName(node, 'name'); const symbolNodes = getNameIdentifiers(name); return { isGlobal, @@ -343,7 +362,7 @@ function getNameIdentifiers(nameNode: SyntaxNode): SyntaxNode[] { ); } - const identNode = nameNode.childForFieldName('identifier')!; + const identNode = requireFieldName(nameNode, 'identifier'); const qualifierNode = nameNode.childForFieldName('qualifier'); if (qualifierNode) { const qualifier = getNameIdentifiers(qualifierNode); From 3c63a73af3a02998d809fd71dffdcb5a6e6bffa5 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:00:54 +0200 Subject: [PATCH 5/6] Remove unused #clientCapabilities field The field was assigned in the constructor but never read. Drop it along with the constructor parameter and the capabilities value destructured from InitializeParams. Co-Authored-By: Claude Opus 4.8 --- eslint.config.js | 4 ++-- server/src/server.ts | 12 +++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index e57e565..d9818f4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -41,7 +41,7 @@ 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, + 'no-unused-private-class-members': 1, '@typescript-eslint/no-unused-vars': [1, { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', @@ -50,7 +50,7 @@ module.exports = [ '@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': 0, + '@typescript-eslint/no-unused-expressions': 1, }, }, ]; diff --git a/server/src/server.ts b/server/src/server.ts index b93885b..84faaa1 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -54,23 +54,17 @@ import { logger, setLoggerOptions } from './util/logger'; */ export class ModelicaServer { #analyzer: Analyzer; - #clientCapabilities: LSP.ClientCapabilities; #connection: LSP.Connection; #documents: LSP.TextDocuments = new LSP.TextDocuments(TextDocument); - private constructor( - analyzer: Analyzer, - clientCapabilities: LSP.ClientCapabilities, - connection: LSP.Connection, - ) { + private constructor(analyzer: Analyzer, connection: LSP.Connection) { this.#analyzer = analyzer; - this.#clientCapabilities = clientCapabilities; this.#connection = connection; } public static async initialize( connection: LSP.Connection, - { capabilities, workspaceFolders, initializationOptions }: LSP.InitializeParams, + { workspaceFolders, initializationOptions }: LSP.InitializeParams, ): Promise { // Initialize logger setLoggerOptions({ @@ -128,7 +122,7 @@ export class ModelicaServer { } logger.debug('Initialized'); - return new ModelicaServer(analyzer, capabilities, connection); + return new ModelicaServer(analyzer, connection); } /** From d46aba961383fe8506b8b1362f3ea7a8dfd97730 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:07:03 +0200 Subject: [PATCH 6/6] Stop tracking TypeScript build cache tsconfig.tsbuildinfo is machine-generated incremental-build cache, not source. Add *.tsbuildinfo to .gitignore and remove the tracked client/ and server/ copies. Co-Authored-By: Claude Opus 4.8 --- client/tsconfig.tsbuildinfo | 1 - server/tsconfig.tsbuildinfo | 1 - 2 files changed, 2 deletions(-) delete mode 100644 client/tsconfig.tsbuildinfo delete mode 100644 server/tsconfig.tsbuildinfo diff --git a/client/tsconfig.tsbuildinfo b/client/tsconfig.tsbuildinfo deleted file mode 100644 index c947e44..0000000 --- a/client/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/extension.ts","./src/test/gotoDeclaration.test.ts","./src/test/helper.ts","./src/test/index.ts","./src/test/mslLibrary.test.ts","./src/test/runTest.ts","./src/test/symbolinformation.test.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/server/tsconfig.tsbuildinfo b/server/tsconfig.tsbuildinfo deleted file mode 100644 index a5ecbdf..0000000 --- a/server/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/analyzer.ts","./src/parser.ts","./src/server.ts","./src/analysis/reference.ts","./src/analysis/resolveReference.ts","./src/analysis/test/resolveReference.test.ts","./src/project/document.ts","./src/project/index.ts","./src/project/library.ts","./src/project/project.ts","./src/project/test/document.test.ts","./src/project/test/project.test.ts","./src/test/analyzer.test.ts","./src/test/server.test.ts","./src/util/declarations.ts","./src/util/index.ts","./src/util/logger.ts","./src/util/tree-sitter.ts","./src/util/test/declarations.test.ts","./src/util/test/util.test.ts"],"version":"6.0.3"} \ No newline at end of file