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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ node_modules/
package-lock.json
*.vsix
.vscode/.enscript-cache.json
/docs
5 changes: 5 additions & 0 deletions debug-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[SERVER STARTED] 2026-03-13T17:49:03.438Z
[runDiagnostics] uri=file:///p%3A/scripts/5_mission/mission/missiongameplay.c
[runDiagnostics] uri=file:///p%3A/scripts/5_mission/mission/missiongameplay.c
[runDiagnostics] uri=file:///p%3A/scripts/5_mission/mission/missiongameplay.c
[checkUnknownSymbols] uri=undefined docCache.size=2904
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "df-enscript",
"displayName": "DaemonForge Enfusion Script",
"description": "DaemonForge fork of Enfusion Script — Enforce/Enscript language support, diagnostics, and DayZ game‑specific tooling.",
"version": "0.2.1",
"version": "0.2.2",
"publisher": "DaemonForge",
"icon": "media/dflogo.png",
"engines": {
Expand Down
138 changes: 136 additions & 2 deletions server/src/analysis/ast/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ export interface ReturnStatementInfo {
exprEnd: number; // Character offset of expression end (before ';')
}

/** A standalone identifier reference found in a function body expression */
export interface BodyIdentifierRef {
name: string;
start: Position;
end: Position;
}

export interface FunctionDeclNode extends SymbolNodeBase {
kind: 'FunctionDecl';
parameters: VarDeclNode[];
Expand All @@ -171,6 +178,8 @@ export interface FunctionDeclNode extends SymbolNodeBase {
returnStatements: ReturnStatementInfo[]; // All return statements found in the body
hasBody: boolean; // true if function has a { } body (not proto/native)
isOverride: boolean; // true if declared with the 'override' keyword
bodyTypeRefs: TypeNode[]; // Type references found in the body (e.g., static call targets: ClassName.Method())
bodyIdentifierRefs: BodyIdentifierRef[]; // Standalone identifiers in expressions (for unknown-symbol checking)
}

export interface File {
Expand Down Expand Up @@ -545,6 +554,27 @@ export function parse(
start: doc.positionAt(enumMemberNameTok.start),
end: doc.positionAt(enumMemberNameTok.end),
} as EnumMemberDeclNode);

while (!eof()) {
const enumSepTok = peek();
if (enumSepTok.value === ',') {
next();
break;
}
if (enumSepTok.value === '}') {
break;
}
if (enumSepTok.value === ';') {
addDiagnostic(
enumSepTok,
'Enum members must be separated by commas, not semicolons.',
DiagnosticSeverity.Error
);
next();
break;
}
next();
}
}
else next();
}
Expand Down Expand Up @@ -644,6 +674,9 @@ export function parse(
// ====================================================================
const locals: VarDeclNode[] = [];
const returnStatements: ReturnStatementInfo[] = [];
const bodyTypeRefs: TypeNode[] = [];
const bodyIdentifierRefs: BodyIdentifierRef[] = [];
const seenIdentifierRefs = new Set<string>(); // dedup
let hasBody = false;
if (peek().value === '{') {
hasBody = true;
Expand All @@ -660,9 +693,22 @@ export function parse(
let prevPrevIdx = -1;
let prev: Token | null = null;
let prevIdx = -1;
// Track the type token for comma-separated multi-variable declarations
// e.g., `float textX, textY, textZ;` — after detecting textX via the
// normal TypeName VarName pattern, commaChainTypeTok remembers `float`
// so that textY and textZ are also registered as locals.
let commaChainTypeTok: Token | null = null;
let commaChainParenDepth = 0; // paren depth at which the chain was created
let parenDepth = 0; // tracks () nesting to prevent false locals inside call args
while (depth > 0 && !eof()) {
const t = next();
const tIdx = pos - 1; // index of the token that next() just returned
// Track parenthesis depth BEFORE local detection so the
// comma chain check sees the correct nesting level.
// This prevents `bool hit = Func(a, b, c);` from falsely
// detecting the call arguments as comma-chain bool locals.
if (t.value === '(') parenDepth++;
else if (t.value === ')') parenDepth = Math.max(0, parenDepth - 1);
if (t.value === '{') {
depth++;
bodyScopes.push([]);
Expand Down Expand Up @@ -789,8 +835,20 @@ export function parse(
// walk back to `<` from a for-loop condition `i < tierCount`, falsely
// detecting `i maxSafeRadius` as a generic-typed variable declaration.
// Valid generic types like `array<int>` never span these boundaries.
if (prev && prevPrev && (t.value === ';' || t.value === '=' || t.value === ',' || t.value === ':' || t.value === '[')) {
//
// '(' is included to detect constructor-style declarations:
// ScriptInputUserData serializer();
// where the variable name is followed by parentheses.
if (prev && prevPrev && (t.value === ';' || t.value === '=' || t.value === ',' || t.value === ':' || t.value === '[' || t.value === '(')) {
let typeTok = prevPrev;
// Check for comma-separated continuation: `float a, b, c;`
// When prevPrev is ',' and we have a stored type from the chain,
// use that type instead of trying to interpret ',' as a type token.
let isCommaChain = false;
if (prevPrev.value === ',' && commaChainTypeTok && prev.kind === TokenKind.Identifier && parenDepth === commaChainParenDepth) {
typeTok = commaChainTypeTok;
isCommaChain = true;
}
if (prevPrev.value === '>' || prevPrev.value === '>>') {
// Walk backwards through tokens to find matching '<' and the type before it
// '>>' counts as 2 closing brackets (nested generics)
Expand Down Expand Up @@ -823,7 +881,7 @@ export function parse(
}
}
}
const isTypeTok = typeTok.kind === TokenKind.Identifier
const isTypeTok = isCommaChain || typeTok.kind === TokenKind.Identifier
|| (typeTok.kind === TokenKind.Keyword && isPrimitiveType(typeTok.value));
const isNameTok = prev.kind === TokenKind.Identifier;
if (isTypeTok && isNameTok) {
Expand Down Expand Up @@ -852,7 +910,81 @@ export function parse(
if (bodyScopes.length > 0) {
bodyScopes[bodyScopes.length - 1].push(local);
}
// Continue or end the comma chain.
// Keep the type alive on ',' (direct: float x, y;) and '='
// (initializer: float x = 0, y = 0;) so that subsequent
// comma-separated variables with assignments are detected.
commaChainTypeTok = (t.value === ',' || t.value === '=') ? typeTok : null;
commaChainParenDepth = parenDepth;
} else {
// Not a valid declaration — only reset comma chain on
// statement boundaries. Non-boundary triggers (like '('
// inside a function call in an initializer expression)
// must not kill the chain.
if (t.value === ';') {
commaChainTypeTok = null;
}
}
} else {
// Reset comma chain on any non-declaration trigger
if (t.value === ';' || t.value === '{' || t.value === '}') {
commaChainTypeTok = null;
}
}

// ================================================================
// STATIC CALL TARGET DETECTION
// ================================================================
// Detect ClassName.Method() patterns: Identifier followed by '.'
// Captures the identifier as a body type reference so that
// cross-module visibility checks can flag violations like
// using a 5_Mission class from 4_World code.
// Only capture if the identifier starts with uppercase (class
// names are PascalCase) to avoid capturing local variables.
// Skip chained property accesses (e.g., context.Player.Do())
// by requiring the token before the identifier is NOT a '.'.
// ================================================================
if (t.value === '.' && prev && prev.kind === TokenKind.Identifier
&& /^[A-Z]/.test(prev.value)
&& (!prevPrev || prevPrev.value !== '.')) {
// Don't record duplicates for the same identifier in this body
if (!bodyTypeRefs.some(r => r.identifier === prev!.value)) {
bodyTypeRefs.push({
kind: 'Type',
uri: doc.uri,
identifier: prev.value,
start: doc.positionAt(prev.start),
end: doc.positionAt(prev.end),
arrayDims: [],
modifiers: [],
});
}
}

// ================================================================
// STANDALONE IDENTIFIER REFERENCE DETECTION
// ================================================================
// Capture identifiers used as values in expressions (not types,
// not call targets, not member-access chains). Used by
// checkUnknownSymbols to flag unresolvable references.
//
// We capture an identifier when it is NOT:
// - preceded by '.' (member access: obj.field)
// - followed by '(' (function call: Func())
// - followed by '.' (static access: Class.Method, tracked by bodyTypeRefs)
// - a keyword (if, return, new, etc.)
// - part of a declaration (Type VarName handled by local detection)
// ================================================================
if (prev && prev.kind === TokenKind.Identifier
&& (!prevPrev || (prevPrev.value !== '.' && prevPrev.value !== '::'))
&& t.value !== '('
&& !seenIdentifierRefs.has(prev.value)) {
seenIdentifierRefs.add(prev.value);
bodyIdentifierRefs.push({
name: prev.value,
start: doc.positionAt(prev.start),
end: doc.positionAt(prev.end),
});
}

prevPrev = prev;
Expand All @@ -872,6 +1004,8 @@ export function parse(
parameters: params,
locals: locals,
returnStatements: returnStatements,
bodyTypeRefs: bodyTypeRefs,
bodyIdentifierRefs: bodyIdentifierRefs,
hasBody: hasBody,
isOverride: mods.includes('override'),
annotations: annotations,
Expand Down
2 changes: 1 addition & 1 deletion server/src/analysis/lexer/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const keywords = new Set([
// Class/type declaration keywords
'class', 'enum', 'typedef', 'using', 'extends',
// Modifiers
'modded', 'proto', 'native', 'owned', 'local', 'auto', 'event',
'modded', 'proto', 'native', 'owned', 'local', 'auto', 'event', 'thread',
'ref', 'reference', 'out', 'inout',
'override', 'private', 'protected', 'public', 'static', 'const',
'notnull', 'external', 'volatile', 'autoptr',
Expand Down
Loading
Loading