This commit is contained in:
2026-08-11 12:01:25 +02:00
parent dbc2c99d8d
commit 2a049c7e92
20 changed files with 1045 additions and 227 deletions
+117 -36
View File
@@ -1,6 +1,10 @@
import * as vscode from "vscode";
import { dirname } from "node:path";
import { LineMap, type XmlElement } from "../language/xmlParser";
import {
LineMap,
type XmlElement,
type XmlParseError,
} from "../language/xmlParser";
import { resolveElementType } from "../language/typeContext";
import { resolveSource, buildSearchPaths } from "../indexer/includeResolver";
import { validateSdkPath } from "../sdk";
@@ -16,6 +20,7 @@ import {
} from "../indexer/refs";
import type { LogicalElement } from "../indexer/logicalTree";
import { scopePathKey } from "../indexer/localScope";
import { t } from "../localize";
export class Ra3Diagnostics {
private collection: vscode.DiagnosticCollection;
@@ -57,7 +62,7 @@ export class Ra3Diagnostics {
new vscode.Position(err.line, err.character),
new vscode.Position(err.line, err.character + 1),
),
err.message,
this.parseErrorMessage(err),
vscode.DiagnosticSeverity.Error,
"xml-syntax",
),
@@ -79,6 +84,42 @@ export class Ra3Diagnostics {
this.collection.set(document.uri, diags);
}
private parseErrorMessage(err: XmlParseError): string {
switch (err.code) {
case "content-before-root":
return t("Content is not allowed before the root element");
case "unterminated-comment":
return t("Unterminated comment");
case "unterminated-cdata":
return t("Unterminated CDATA section");
case "unterminated-doctype":
return t("Unterminated DOCTYPE");
case "unterminated-processing-instruction":
return t("Unterminated processing instruction");
case "unterminated-closing-tag":
return t("Unterminated closing tag");
case "unterminated-start-tag":
return t("Unterminated start tag");
case "malformed-markup":
return t("Malformed markup");
case "unexpected-closing-tag":
return t(
"Unexpected closing tag </{0}>",
err.params?.name ?? "",
);
case "mismatched-closing-tag":
return t(
"Mismatched closing tag: expected </{0}>, found </{1}>",
err.params?.expected ?? "",
err.params?.found ?? "",
);
case "element-never-closed":
return t("Element <{0}> is never closed", err.params?.name ?? "");
default:
return err.message;
}
}
clear(uri: vscode.Uri): void {
this.collection.delete(uri);
}
@@ -120,7 +161,7 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
range,
`Top-level asset <${local}> requires an id attribute`,
t("Top-level asset <{0}> requires an id attribute", local),
vscode.DiagnosticSeverity.Error,
"missing-id",
),
@@ -136,7 +177,12 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
where,
`Duplicate id "${idAttr.value}" for <${local}> (also defined on line ${prev.line})`,
t(
'Duplicate id "{0}" for <{1}> (also defined on line {2})',
idAttr.value,
local,
prev.line,
),
vscode.DiagnosticSeverity.Error,
"duplicate-id",
),
@@ -166,7 +212,7 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
range,
`Unknown element <${local}> (not in the RA3 XSD model)`,
t("Unknown element <{0}> (not in the RA3 XSD model)", local),
vscode.DiagnosticSeverity.Warning,
"unknown-element",
),
@@ -187,16 +233,16 @@ export class Ra3Diagnostics {
continue;
}
if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
diags.push(
this.diag(
new vscode.Range(
document.positionAt(attr.nameStart),
document.positionAt(attr.nameEnd),
diags.push(
this.diag(
new vscode.Range(
document.positionAt(attr.nameStart),
document.positionAt(attr.nameEnd),
),
t('Unknown attribute "{0}" for <{1}>', aName, local),
vscode.DiagnosticSeverity.Warning,
"unknown-attribute",
),
`Unknown attribute "${aName}" for <${local}>`,
vscode.DiagnosticSeverity.Warning,
"unknown-attribute",
),
);
}
@@ -258,8 +304,12 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
range,
`Duplicate id "${id}" for <${type}> (also defined in ${other.file})` +
(provisional ? " (based on a partial index)" : ""),
t(
'Duplicate id "{0}" for <{1}> (also defined in {2})',
id,
type,
other.file,
) + (provisional ? t(" (based on a partial index)") : ""),
vscode.DiagnosticSeverity.Error,
"duplicate-id",
),
@@ -296,8 +346,8 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
range,
`Undefined define "$${m[1]}"` +
(provisional ? " (index incomplete — may be a false positive)" : ""),
t('Undefined define "${0}"', m[1]) +
(provisional ? t(" (index incomplete — may be a false positive)") : ""),
vscode.DiagnosticSeverity.Warning,
code,
),
@@ -318,20 +368,18 @@ export class Ra3Diagnostics {
const attrRef = model
.attributesOfType(elType)
.find((a) => a.name === attrName);
const expected = attrRef?.refType
? `of type \`${attrRef.refType}\``
: attrRef?.isRef
? "of the expected declared type"
: "matching";
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
const baseMessage = anyDef
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
: `Unresolved reference "${value}" (not found in the current index)`;
const baseMessage = unresolvedReferenceMessage(
value,
anyDef,
attrRef?.refType ?? null,
attrRef?.isRef ?? false,
);
diags.push(
this.diag(
range,
provisional
? `${baseMessage} (index incomplete — may be a false positive)`
? baseMessage + t(" (index incomplete — may be a false positive)")
: baseMessage,
severity === "warning"
? vscode.DiagnosticSeverity.Warning
@@ -379,8 +427,8 @@ export class Ra3Diagnostics {
diags.push(
this.diag(
range,
`Undefined define "$${m[1]}"` +
(provisional ? " (index incomplete — may be a false positive)" : ""),
t('Undefined define "${0}"', m[1]) +
(provisional ? t(" (index incomplete — may be a false positive)") : ""),
vscode.DiagnosticSeverity.Warning,
code,
),
@@ -398,16 +446,18 @@ export class Ra3Diagnostics {
(idx.local?.assetsById.has(value.toLowerCase()) ?? false) ||
idx.assetsById.has(value.toLowerCase());
const refType = info.refType;
const expected = refType ? `of type \`${refType}\`` : "of the expected declared type";
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
const baseMessage = anyDef
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
: `Unresolved reference "${value}" (not found in the current index)`;
const baseMessage = unresolvedReferenceMessage(
value,
anyDef,
refType ?? null,
!refType,
);
diags.push(
this.diag(
range,
provisional
? `${baseMessage} (index incomplete — may be a false positive)`
? baseMessage + t(" (index incomplete — may be a false positive)")
: baseMessage,
severity === "warning"
? vscode.DiagnosticSeverity.Warning
@@ -432,7 +482,10 @@ export class Ra3Diagnostics {
document.positionAt(typeAttr.valueStart),
document.positionAt(typeAttr.valueEnd),
),
`Invalid Include type "${typeAttr.value}" (expected reference, instance or all)`,
t(
'Invalid Include type "{0}" (expected reference, instance or all)',
typeAttr.value,
),
vscode.DiagnosticSeverity.Error,
"include-type",
),
@@ -462,7 +515,7 @@ export class Ra3Diagnostics {
document.positionAt(sourceAttr.valueStart),
document.positionAt(sourceAttr.valueEnd),
),
`Include target not found: ${sourceAttr.value}`,
t("Include target not found: {0}", sourceAttr.value),
vscode.DiagnosticSeverity.Warning,
"include-not-found",
),
@@ -488,6 +541,34 @@ function localName(tag: string): string {
return idx >= 0 ? tag.slice(idx + 1) : tag;
}
function unresolvedReferenceMessage(
value: string,
anyDef: boolean,
refType: string | null,
isRef: boolean,
): string {
if (anyDef) {
if (refType) {
return t(
'Reference "{0}" has no definition of type `{1}` (ids with the same name exist for other types)',
value,
refType,
);
}
if (isRef) {
return t(
'Reference "{0}" has no definition of the expected declared type (ids with the same name exist for other types)',
value,
);
}
return t(
'Reference "{0}" has no matching definition (ids with the same name exist for other types)',
value,
);
}
return t('Unresolved reference "{0}" (not found in the current index)', value);
}
function tagRange(document: vscode.TextDocument, el: XmlElement): vscode.Range {
return new vscode.Range(
document.positionAt(el.start),