From 10911ae2466ef2a8ac997b8663193e824f6d4352 Mon Sep 17 00:00:00 2001 From: Oleksandra Kordonets Date: Thu, 11 Dec 2025 19:08:20 -0500 Subject: [PATCH 1/3] Add model-aware context and utilities --- server/src/context.ts | 196 ++++++++++++++++++++++++++++++++++++++++-- server/src/helpers.ts | 9 +- 2 files changed, 192 insertions(+), 13 deletions(-) diff --git a/server/src/context.ts b/server/src/context.ts index 1e70d91..25c112b 100644 --- a/server/src/context.ts +++ b/server/src/context.ts @@ -1,3 +1,5 @@ +import { existsSync } from "fs"; +import path, { dirname, join, normalize } from "path"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Connection, @@ -39,6 +41,15 @@ export type DottedName = string; // TODO: use the publicodes types export type RawPublicodes = Record; +export type Model = { + root: FilePath; + ruleToFileNameMap: Map; + rawPublicodesRules: RawPublicodes; + parsedRules: Record; + engine: Engine; + files: Set; +}; + export type FileInfos = { // List of rules in the file extracted from the tree-sitter's CST ruleDefs: RuleDef[]; @@ -82,12 +93,11 @@ export type LSContext = { diagnosticsURI: Set; documentSettings: Map>; documents: TextDocuments; - engine: Engine; + fileToModel: Map; fileInfos: Map; globalSettings: DocumentSettings; - parsedRules: Record; - rawPublicodesRules: RawPublicodes; - ruleToFileNameMap: Map; + models: Map; + workspaceFolders: FilePath[]; }; /** @@ -98,10 +108,13 @@ export type LSContext = { export function getRuleDef( ctx: LSContext, ruleName: string, + fromFilePath?: FilePath, ): RuleDef | undefined { - const fileName = ctx.ruleToFileNameMap.get(ruleName); + const fileName = getRuleFilePath(ctx, ruleName, fromFilePath); if (!fileName) { - ctx.connection.console.error(`[getRuleDefPos] file not found: ${ruleName}`); + ctx.connection.console.error( + `[getRuleDefPos] file not found: ${ruleName}`, + ); return; } @@ -109,3 +122,174 @@ export function getRuleDef( .get(fileName) ?.ruleDefs.find((rule) => rule.dottedName === ruleName); } + +export const PUBLICODES_CONFIG_FILES = [ + ".publicodes.config.ts", + ".publicodes.config.js", +]; + +function findWorkspaceFolder( + ctx: LSContext, + filePath: FilePath, +): FilePath | undefined { + const normalizedFile = normalize(filePath); + + const matchingFolders = ctx.workspaceFolders.filter((folder) => { + const normalizedFolder = normalize(folder); + + return ( + normalizedFile === normalizedFolder || + normalizedFile.startsWith(`${normalizedFolder}${path.sep}`) + ); + }); + + return matchingFolders.sort((a, b) => b.length - a.length)[0]; +} + +export function findModelRoot(ctx: LSContext, filePath: FilePath): FilePath { + const workspaceRoot = + findWorkspaceFolder(ctx, filePath) ?? dirname(normalize(filePath)); + + let currentDir = dirname(normalize(filePath)); + + while (true) { + if ( + PUBLICODES_CONFIG_FILES.some((configFile) => + existsSync(join(currentDir, configFile)), + ) + ) { + return currentDir; + } + + if (existsSync(join(currentDir, "package.json"))) { + return currentDir; + } + + if (currentDir === workspaceRoot) { + break; + } + + const parent = dirname(currentDir); + if (parent === currentDir) { + break; + } + currentDir = parent; + } + + return workspaceRoot; +} + +export function getModelFromFile( + ctx: LSContext, + filePath: FilePath, +): Model | undefined { + const modelRoot = ctx.fileToModel.get(filePath); + + if (modelRoot == undefined) { + return undefined; + } + + return ctx.models.get(modelRoot); +} + +export function ensureModelForFile( + ctx: LSContext, + filePath: FilePath, +): Model { + const modelRoot = findModelRoot(ctx, filePath); + const currentModelRoot = ctx.fileToModel.get(filePath); + + if (currentModelRoot && currentModelRoot !== modelRoot) { + const currentModel = ctx.models.get(currentModelRoot); + currentModel?.files.delete(filePath); + currentModel?.ruleToFileNameMap.forEach((path, rule) => { + if (path === filePath) { + currentModel.ruleToFileNameMap.delete(rule); + } + }); + } + + let model = ctx.models.get(modelRoot); + + if (model == undefined) { + model = { + root: modelRoot, + ruleToFileNameMap: new Map(), + rawPublicodesRules: {}, + parsedRules: {}, + engine: new Engine({}), + files: new Set(), + }; + ctx.models.set(modelRoot, model); + } + + model.files.add(filePath); + ctx.fileToModel.set(filePath, modelRoot); + + return model; +} + +export function clearRuleIndexForFile(ctx: LSContext, filePath: FilePath) { + const model = getModelFromFile(ctx, filePath); + + if (model == undefined) { + return; + } + + model.ruleToFileNameMap.forEach((path, rule) => { + if (path === filePath) { + model.ruleToFileNameMap.delete(rule); + } + }); +} + +export function removeFileFromModel(ctx: LSContext, filePath: FilePath) { + const modelRoot = ctx.fileToModel.get(filePath); + if (modelRoot == undefined) { + return; + } + + const model = ctx.models.get(modelRoot); + if (model == undefined) { + return; + } + + clearRuleIndexForFile(ctx, filePath); + model.files.delete(filePath); + ctx.fileToModel.delete(filePath); + + if (model.files.size === 0) { + ctx.models.delete(modelRoot); + } +} + +export function getRuleFilePath( + ctx: LSContext, + ruleName: string, + fromFilePath?: FilePath, +): FilePath | undefined { + const model = + fromFilePath != undefined ? getModelFromFile(ctx, fromFilePath) : undefined; + + if (model) { + return model.ruleToFileNameMap.get(ruleName); + } + + let foundFile: FilePath | undefined = undefined; + + ctx.models.forEach((model) => { + const file = model.ruleToFileNameMap.get(ruleName); + + if (!file) { + return; + } + + if (foundFile && file !== foundFile) { + foundFile = undefined; + return; + } + foundFile = file; + }); + + return foundFile; +} diff --git a/server/src/helpers.ts b/server/src/helpers.ts index 7bc2d62..b00e3ff 100644 --- a/server/src/helpers.ts +++ b/server/src/helpers.ts @@ -1,6 +1,5 @@ -import TSParser from "tree-sitter"; import { Range, URI } from "vscode-languageserver"; -import { LSContext, Position } from "./context"; +import { LSContext, Position, removeFileFromModel } from "./context"; import { fileURLToPath } from "node:url"; /** @@ -43,11 +42,7 @@ export function deleteFileFromCtx(ctx: LSContext, uri: URI) { ctx.fileInfos.delete(path); - ctx.ruleToFileNameMap.forEach((path, rule) => { - if (path === fileURLToPath(uri)) { - ctx.ruleToFileNameMap.delete(rule); - } - }); + removeFileFromModel(ctx, path); if (ctx.diagnostics.has(path)) { ctx.diagnostics.delete(path); From 38e7a7e242905ab31bbef6745077c5e3515d616b Mon Sep 17 00:00:00 2001 From: Oleksandra Kordonets Date: Thu, 11 Dec 2025 19:08:35 -0500 Subject: [PATCH 2/3] Scope parsing and validation and language features per model --- server/src/codeAction.ts | 2 +- server/src/completion.ts | 9 +-- server/src/initialized.ts | 5 +- server/src/onDefinition.ts | 8 ++- server/src/onHover.ts | 11 ++- server/src/parseRules.ts | 11 ++- server/src/server.ts | 35 ++++++---- server/src/treeSitter.ts | 6 +- server/src/validate.ts | 138 ++++++++++++++++++++++--------------- 9 files changed, 140 insertions(+), 85 deletions(-) diff --git a/server/src/codeAction.ts b/server/src/codeAction.ts index 995c7a1..6d055b8 100644 --- a/server/src/codeAction.ts +++ b/server/src/codeAction.ts @@ -97,7 +97,7 @@ export function createRule( ctx: LSContext, { uri, ruleName, ruleNameToCreate, position }: CreateRuleParams, ) { - const ruleDefPos = getRuleDef(ctx, ruleName)?.defPos; + const ruleDefPos = getRuleDef(ctx, ruleName, fileURLToPath(uri))?.defPos; if (!ruleDefPos) { ctx.connection.console.error( diff --git a/server/src/completion.ts b/server/src/completion.ts index 0dc2eec..e712687 100644 --- a/server/src/completion.ts +++ b/server/src/completion.ts @@ -7,7 +7,7 @@ import { MarkupKind, ServerRequestHandler, } from "vscode-languageserver/node.js"; -import { DottedName, LSContext } from "./context"; +import { DottedName, LSContext, getModelFromFile } from "./context"; import { RuleNode } from "publicodes"; import { mechanisms } from "./completion-items/mechanisms"; import { keywords } from "./completion-items/keywords"; @@ -92,8 +92,9 @@ export function completionHandler( return keywordsAndMechanismsCompletionItems; } + const model = getModelFromFile(ctx, filePath); const completionItems = getRuleCompletionItems( - ctx, + model?.parsedRules ?? {}, refNodeAtCursorPosition, params.context?.triggerKind ?? CompletionTriggerKind.Invoked, fullRefName, @@ -130,7 +131,7 @@ export function completionResolveHandler(_ctx: LSContext) { * @param currentRefNode The current node currenlty in completion which allows to filter only corresponding childs rules */ const getRuleCompletionItems = ( - ctx: LSContext, + parsedRules: Record, currentRefNode: TSParser.SyntaxNode, triggerKind: CompletionTriggerKind, currRuleName: DottedName | undefined, @@ -211,7 +212,7 @@ const getRuleCompletionItems = ( return hasCommonNamespace; }; - return Object.entries(ctx.parsedRules) + return Object.entries(parsedRules) .filter(([dottedName, _]) => { const splittedDottedName = dottedName.split(" . "); diff --git a/server/src/initialized.ts b/server/src/initialized.ts index 0a57b14..9083729 100644 --- a/server/src/initialized.ts +++ b/server/src/initialized.ts @@ -2,7 +2,7 @@ import { DidChangeConfigurationNotification } from "vscode-languageserver/node.j import { LSContext } from "./context"; import { parseDir } from "./parseRules"; import validate from "./validate"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; export default function intializedHandler(ctx: LSContext) { return () => { @@ -17,6 +17,9 @@ export default function intializedHandler(ctx: LSContext) { if (ctx.config.hasWorkspaceFolderCapability) { ctx.connection.workspace.getWorkspaceFolders().then((folders) => { if (folders) { + ctx.workspaceFolders = folders.map((folder) => + fileURLToPath(folder.uri), + ); folders.forEach((folder) => { parseDir(ctx, folder.uri); }); diff --git a/server/src/onDefinition.ts b/server/src/onDefinition.ts index 9f3362e..a9baa51 100644 --- a/server/src/onDefinition.ts +++ b/server/src/onDefinition.ts @@ -4,7 +4,7 @@ import { HandlerResult, LocationLink, } from "vscode-languageserver"; -import { LSContext } from "./context"; +import { LSContext, getRuleFilePath } from "./context"; import { getFullRefName } from "./treeSitter"; import { fileURLToPath, pathToFileURL } from "url"; @@ -47,7 +47,11 @@ export default function (ctx: LSContext) { fileURLToPath(textDocument.uri), node, ); - const filePath = ctx.ruleToFileNameMap.get(fullRefName); + const filePath = getRuleFilePath( + ctx, + fullRefName, + fileURLToPath(textDocument.uri), + ); if (filePath == undefined) { return []; } diff --git a/server/src/onHover.ts b/server/src/onHover.ts index c3df02c..e5c7084 100644 --- a/server/src/onHover.ts +++ b/server/src/onHover.ts @@ -1,5 +1,5 @@ import { HandlerResult, Hover, HoverParams } from "vscode-languageserver"; -import { LSContext } from "./context"; +import { LSContext, getModelFromFile } from "./context"; import { getFullRefName } from "./treeSitter"; import { fileURLToPath } from "url"; import { serializeEvaluation } from "publicodes"; @@ -32,14 +32,19 @@ export default function (ctx: LSContext) { switch (node.type) { case "name": { try { + const model = getModelFromFile(ctx, fileURLToPath(textDocument.uri)); + if (!model) { + return; + } + const fullRefName = getFullRefName( ctx, fileURLToPath(textDocument.uri), node, ); - const rawRule = ctx.rawPublicodesRules[fullRefName]; - const nodeValue = ctx.engine.evaluate(fullRefName); + const rawRule = model.rawPublicodesRules[fullRefName]; + const nodeValue = model.engine.evaluate(fullRefName); // TODO: polish the hover message const value = `**${rawRule?.titre ?? fullRefName}** (${serializeEvaluation(nodeValue)}) diff --git a/server/src/parseRules.ts b/server/src/parseRules.ts index e56a871..a526138 100644 --- a/server/src/parseRules.ts +++ b/server/src/parseRules.ts @@ -12,6 +12,8 @@ import { LSContext, RawPublicodes, RuleDef, + clearRuleIndexForFile, + ensureModelForFile, } from "./context"; import { getTSTree } from "./treeSitter"; import { mapAppend, positionToRange, trimQuotedString } from "./helpers"; @@ -58,13 +60,16 @@ export function parseDocument( filePath: FilePath, document?: TextDocument, ) { + const model = ensureModelForFile(ctx, filePath); + clearRuleIndexForFile(ctx, filePath); + const fileContent = document?.getText() ?? readFileSync(filePath).toString(); const tsTree = getTSTree(fileContent); const { rawRules, errors } = parseRawRules(filePath); const { definitions, importNamespace } = collectRuleDefs(tsTree.rootNode); const ruleDefs = definitions.filter(({ dottedName, namesPos }) => { - const ruleFilePath = ctx.ruleToFileNameMap.get(dottedName); + const ruleFilePath = model.ruleToFileNameMap.get(dottedName); // Check if the rule is already defined in another file // TODO: add a test case for this @@ -88,7 +93,7 @@ La règle '${dottedName}' est déjà définie dans le fichier : '${ruleFilePath} // Checks if the namespace is not already defined in another file // TODO: add a warning if the namespace is already defined in another file if (importNamespace) { - const ruleFilePath = ctx.ruleToFileNameMap.get(importNamespace); + const ruleFilePath = model.ruleToFileNameMap.get(importNamespace); if (ruleFilePath && ruleFilePath !== filePath) { delete rawRules[importNamespace]; } @@ -102,7 +107,7 @@ La règle '${dottedName}' est déjà définie dans le fichier : '${ruleFilePath} }); ruleDefs.forEach(({ dottedName }) => { - ctx.ruleToFileNameMap.set(dottedName, filePath); + model.ruleToFileNameMap.set(dottedName, filePath); }); if (errors.length > 0) { diff --git a/server/src/server.ts b/server/src/server.ts index c96be5f..8f53c34 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -8,7 +8,12 @@ import { } from "vscode-languageserver/node.js"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { LSContext, defaultDirsToIgnore, defaultDocSettings } from "./context"; +import { + LSContext, + defaultDirsToIgnore, + defaultDocSettings, + ensureModelForFile, +} from "./context"; import initialize from "./initialize"; import initializedHandler from "./initialized"; import { completionHandler, completionResolveHandler } from "./completion"; @@ -17,7 +22,6 @@ import validate from "./validate"; import onDefinitionHandler from "./onDefinition"; import onHoverHandler from "./onHover"; import { semanticTokensFullProvider } from "./semanticTokens"; -import Engine from "publicodes"; import { fileURLToPath } from "node:url"; import { deleteFileFromCtx, positionToRange } from "./helpers"; import { @@ -43,14 +47,13 @@ let ctx: LSContext = { hasWorkspaceFolderCapability: false, hasDiagnosticRelatedInformationCapability: false, }, - engine: new Engine({}), + fileToModel: new Map(), fileInfos: new Map(), diagnostics: new Map(), - ruleToFileNameMap: new Map(), diagnosticsURI: new Set(), - rawPublicodesRules: {}, - parsedRules: {}, dirsToIgnore: defaultDirsToIgnore, + models: new Map(), + workspaceFolders: [], }; ctx.connection.onInitialize((params: InitializeParams) => { @@ -126,9 +129,17 @@ ctx.connection.workspace.onDidRenameFiles((e) => { return; } + const diagnostics = ctx.diagnostics.get(oldFilePath); + + deleteFileFromCtx(ctx, oldUri); + ctx.fileInfos.set(newFilePath, fileInfo); - const diagnostics = ctx.diagnostics.get(oldFilePath); + const model = ensureModelForFile(ctx, newFilePath); + fileInfo.ruleDefs.forEach(({ dottedName }) => + model.ruleToFileNameMap.set(dottedName, newFilePath), + ); + if (diagnostics != undefined) { ctx.diagnostics.set(newFilePath, diagnostics); ctx.diagnosticsURI.add(newUri); @@ -137,15 +148,9 @@ ctx.connection.workspace.onDidRenameFiles((e) => { diagnostics, }); } - - ctx.ruleToFileNameMap.forEach((filePath, rule) => { - if (filePath === oldFilePath) { - ctx.ruleToFileNameMap.set(rule, newFilePath); - } - }); - - deleteFileFromCtx(ctx, oldUri); }); + + validate(ctx); }); ctx.connection.onCodeAction((params) => codeActionHandler(ctx, params)); diff --git a/server/src/treeSitter.ts b/server/src/treeSitter.ts index 98a0beb..30c2165 100644 --- a/server/src/treeSitter.ts +++ b/server/src/treeSitter.ts @@ -1,6 +1,6 @@ import TSParser, { SyntaxNode } from "tree-sitter"; import Publicodes from "tree-sitter-publicodes"; -import { DottedName, LSContext } from "./context"; +import { DottedName, LSContext, getModelFromFile } from "./context"; import { utils } from "publicodes"; import assert from "assert"; import { trimQuotedString } from "./helpers"; @@ -66,8 +66,10 @@ export function getFullRefName( ); } + const parsedRules = getModelFromFile(ctx, filePath)?.parsedRules ?? {}; + return utils.disambiguateReference( - ctx.parsedRules, + parsedRules, ruleDottedName, trimQuotedString(ruleNames.reverse().join(" . ")), ); diff --git a/server/src/validate.ts b/server/src/validate.ts index d871ab7..fffd413 100644 --- a/server/src/validate.ts +++ b/server/src/validate.ts @@ -1,5 +1,11 @@ import { TextDocument } from "vscode-languageserver-textdocument"; -import { FilePath, getRuleDef, LSContext } from "./context"; +import { + FilePath, + LSContext, + ensureModelForFile, + getRuleDef, + getRuleFilePath, +} from "./context"; import Engine from "publicodes"; import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver/node.js"; import { parseDocument } from "./parseRules"; @@ -18,7 +24,7 @@ export default async function validate( document?: TextDocument, ): Promise { ctx.diagnostics = new Map(); - let startTimer = Date.now(); + const startTimer = Date.now(); if (document) { const docFilePath = fileURLToPath(document.uri); @@ -26,49 +32,62 @@ export default async function validate( parseDocument(ctx, docFilePath, document); } - try { - // Merge all raw rules (from all files) into one object - // NOTE: a better way could be found? - ctx.rawPublicodesRules = {}; - ctx.fileInfos.forEach((fileInfo) => { - ctx.rawPublicodesRules = { - ...ctx.rawPublicodesRules, - ...fileInfo.rawRules, - }; - }); + ctx.fileInfos.forEach((_fileInfo, filePath) => { + ensureModelForFile(ctx, filePath); + }); - ctx.engine = new Engine(ctx.rawPublicodesRules, { - logger: getDiagnosticsLogger(ctx), - }); - ctx.connection.console.log( - `[validate] Engine created in ${Date.now() - startTimer}ms.`, - ); - ctx.parsedRules = ctx.engine.getParsedRules(); - - startTimer = Date.now(); - // Evaluates all the rules to get unit warning - // PERF: this took ~1500ms for 2009 rules, this needs to be optimized - Object.keys(ctx.parsedRules).forEach((rule) => { - ctx.engine.evaluate(rule); - }); - ctx.connection.console.log( - `[validate] Rules evaluated in ${Date.now() - startTimer}ms (${Object.keys(ctx.parsedRules).length} rules).`, - ); - - // Remove previous diagnostics - ctx.diagnosticsURI.forEach((uri) => { - ctx.connection.sendDiagnostics({ uri, diagnostics: [] }); - ctx.diagnosticsURI = new Set(); + ctx.models.forEach((model) => { + model.rawPublicodesRules = {}; + model.parsedRules = {}; + model.ruleToFileNameMap.clear(); + }); + + ctx.fileInfos.forEach((fileInfo, filePath) => { + const model = ensureModelForFile(ctx, filePath); + fileInfo.ruleDefs.forEach(({ dottedName }) => { + model.ruleToFileNameMap.set(dottedName, filePath); }); - } catch (e: any) { - if (e instanceof Error) { - const { filePath, diagnostic } = getDiagnosticFromErrorMsg( - ctx, - e.message, + model.rawPublicodesRules = { + ...model.rawPublicodesRules, + ...fileInfo.rawRules, + }; + }); + + ctx.diagnosticsURI.forEach((uri) => { + ctx.connection.sendDiagnostics({ uri, diagnostics: [] }); + }); + ctx.diagnosticsURI = new Set(); + + ctx.models.forEach((model) => { + const engineStart = Date.now(); + try { + model.engine = new Engine(model.rawPublicodesRules, { + logger: getDiagnosticsLogger(ctx, model.root), + }); + ctx.connection.console.log( + `[validate] Engine created for ${model.root} in ${Date.now() - engineStart}ms.`, ); - mapAppend(ctx.diagnostics, filePath, diagnostic); + + model.parsedRules = model.engine.getParsedRules(); + + const evalStart = Date.now(); + Object.keys(model.parsedRules).forEach((rule) => { + model.engine.evaluate(rule); + }); + ctx.connection.console.log( + `[validate] Rules evaluated for ${model.root} in ${Date.now() - evalStart}ms (${Object.keys(model.parsedRules).length} rules).`, + ); + } catch (e: any) { + if (e instanceof Error) { + const { filePath, diagnostic } = getDiagnosticFromErrorMsg( + ctx, + model.root, + e.message, + ); + mapAppend(ctx.diagnostics, filePath, diagnostic); + } } - } + }); ctx.connection.console.log( `[validate] Found ${ctx.diagnostics.size} diagnostics in ${Date.now() - startTimer}ms.`, @@ -89,7 +108,7 @@ function sendDiagnostics(ctx: LSContext) { }); } -function getDiagnosticsLogger(ctx: LSContext): Logger { +function getDiagnosticsLogger(ctx: LSContext, modelRoot: FilePath): Logger { return { log(msg: string) { ctx.connection.console.log(`[publicodes:log] ${msg}`); @@ -97,13 +116,18 @@ function getDiagnosticsLogger(ctx: LSContext): Logger { warn(msg: string) { const { filePath, diagnostic } = getDiagnosticFromErrorMsg( ctx, + modelRoot, msg, DiagnosticSeverity.Warning, ); mapAppend(ctx.diagnostics, filePath, diagnostic); }, error(msg: string) { - const { filePath, diagnostic } = getDiagnosticFromErrorMsg(ctx, msg); + const { filePath, diagnostic } = getDiagnosticFromErrorMsg( + ctx, + modelRoot, + msg, + ); mapAppend(ctx.diagnostics, filePath, diagnostic); }, }; @@ -111,6 +135,7 @@ function getDiagnosticsLogger(ctx: LSContext): Logger { function getDiagnosticFromErrorMsg( ctx: LSContext, + modelRoot: FilePath, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error, ): { filePath: FilePath | undefined; diagnostic: Diagnostic } { @@ -128,7 +153,12 @@ function getDiagnosticFromErrorMsg( }, }; } - const filePath = ctx.ruleToFileNameMap.get(wrongRule); + + const model = ctx.models.get(modelRoot); + const filePath = + model?.ruleToFileNameMap.get(wrongRule) ?? + getRuleFilePath(ctx, wrongRule); + if (!filePath) { return { filePath: undefined, @@ -143,9 +173,9 @@ function getDiagnosticFromErrorMsg( }; } - if (message.includes(`✖️ La référence "`)) { + if (message.includes(`ƒo-‹÷? La rǸfǸrence "`)) { const refName = message.match( - /✖️ La référence "(.*)" est introuvable/, + /ƒo-‹÷? La rǸfǸrence "(.*)" est introuvable/, )?.[1]; if (refName) { @@ -162,10 +192,10 @@ function getDiagnosticFromErrorMsg( start: refNode.startPosition, end: refNode.endPosition, }), - message: `La référence "${refName}" est introuvable. + message: `La rǸfǸrence "${refName}" est introuvable. [ Solution ] -- Vérifiez que la référence "${refName}" est bien écrite.`, +- VǸrifiez que la rǸfǸrence "${refName}" est bien Ǹcrite.`, code: PublicodesDiagnosticCode.UNKNOWN_REF, data: { ruleName: wrongRule, @@ -177,13 +207,13 @@ function getDiagnosticFromErrorMsg( } } - if (message.includes(`✖️ La règle parente "`)) { + if (message.includes(`ƒo-‹÷? La rÇùgle parente "`)) { const parentRule = message.match( - /✖️ La règle parente "(.*)" n'existe pas/, + /ƒo-‹÷? La rÇùgle parente "(.*)" n'existe pas/, )?.[1]; if (parentRule) { - const ruleDef = getRuleDef(ctx, wrongRule); + const ruleDef = getRuleDef(ctx, wrongRule, filePath); if (ruleDef) { return { @@ -191,10 +221,10 @@ function getDiagnosticFromErrorMsg( diagnostic: { severity, range: positionToRange(ruleDef.namesPos), - message: `La règle parente "${parentRule}" est introuvable. + message: `La rÇùgle parente "${parentRule}" est introuvable. [ Solution ] -- Vérifiez que la règle parente "${parentRule}" est bien écrite.`, +- VǸrifiez que la rÇùgle parente "${parentRule}" est bien Ǹcrite.`, code: PublicodesDiagnosticCode.UNKNOWN_PARENT, data: { ruleName: wrongRule, @@ -224,7 +254,7 @@ function getDiagnosticFromErrorMsg( } function getPublicodeRuleNameFromErrorMsg(msg: string) { - const match = msg.match(/➡️ Dans la règle "([^\n\r]*)"/); + const match = msg.match(/ƒz­‹÷? Dans la rÇùgle "([^\n\r]*)"/); if (match == null) { return undefined; } From 38e372644159f98b0af8b9b31c57f06021651beb Mon Sep 17 00:00:00 2001 From: Oleksandra Kordonets Date: Fri, 12 Dec 2025 20:52:51 -0500 Subject: [PATCH 3/3] Fix diagnostics lookup and guard document symbols --- server/src/server.ts | 18 ++++++++++-------- server/src/validate.ts | 12 +++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 8f53c34..3ae1d66 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -181,12 +181,14 @@ ctx.connection.onDocumentSymbol((params) => { return []; } - return fileInfo.ruleDefs.map(({ dottedName, namesPos, defPos }) => { - return { - name: dottedName, - kind: SymbolKind.Namespace, - range: positionToRange(defPos), - selectionRange: positionToRange(namesPos), - }; - }); + return fileInfo.ruleDefs + .filter((r) => r.defPos != undefined && r.namesPos != undefined) + .map(({ dottedName, namesPos, defPos }) => { + return { + name: dottedName, + kind: SymbolKind.Namespace, + range: positionToRange(defPos), + selectionRange: positionToRange(namesPos), + }; + }); }); diff --git a/server/src/validate.ts b/server/src/validate.ts index fffd413..0666948 100644 --- a/server/src/validate.ts +++ b/server/src/validate.ts @@ -157,7 +157,8 @@ function getDiagnosticFromErrorMsg( const model = ctx.models.get(modelRoot); const filePath = model?.ruleToFileNameMap.get(wrongRule) ?? - getRuleFilePath(ctx, wrongRule); + getRuleFilePath(ctx, wrongRule) ?? + findRuleFilePathFallback(ctx, wrongRule); if (!filePath) { return { @@ -260,3 +261,12 @@ function getPublicodeRuleNameFromErrorMsg(msg: string) { } return match[1]; } + +function findRuleFilePathFallback(ctx: LSContext, ruleName: string) { + for (const [filePath, fileInfo] of ctx.fileInfos.entries()) { + if (fileInfo.ruleDefs.some((r) => r.dottedName === ruleName)) { + return filePath; + } + } + return undefined; +}