# F-04 — functionValidator.js: Line-skip guards and standalone-call detection compensate for missing AST context

**Severity:** Major  
**File:** `core/src/validators/functionValidator.js`  
**Category:** Compensating for incomplete AST use  
**Remediation appropriate:** Yes — Tier 2 (some parts Tier 1)

---

## Context

`functionValidator.js` is the most complex validator and the largest consumer of string-level
parsing. It operates by scanning every line of the document as raw text, applying regex filters
to decide what to examine, then using another regex (`functionPattern`) to find function call
patterns. Almost none of this uses the AST, even though the AST classifies every line's intent
precisely.

---

## Finding 1 — `shouldSkipFunctionValidationLine()` duplicates AST node-type information

**Lines:** 26–46**

```javascript
function shouldSkipFunctionValidationLine(lineText) {
    if (/^\s*#\s*NOTE\b/i.test(lineText)) return true;
    if (/^\s*#?\s*function\s+/i.test(lineText)) return true;
    if (/^\s*#?\s*macro\s+/i.test(lineText)) return true;
    if (parseIncludeDirective(lineText)) return true;
    return false;
}
```

Each pattern detects a line type that the AST has already classified:

| Regex pattern | AST node type |
|---|---|
| `#NOTE` prefix | `CommentLine` (HASH_PREFIX + COMMENT token) |
| `FUNCTION ...` | `FunctionDeclaration` |
| `MACRO ...` | `MacroDeclaration` |
| `INCLUDE/INCLUDEONCE/REQUIRE` | `IncludeDirective` / `RequireDirective` |

The validator calls this for every line, every validation pass, parsing the same raw text the
lexer has already tokenised.

**Why this pattern exists:** The validator has no access to the AST node for the current line.
It only receives the raw `document` and iterates `lineIndex`. The AST is available (via
`buildDocumentAst`) but no line-to-node mapping is threaded into the validator.

**Recommended fix (Tier 2):** Extend `documentFacts` with a per-line node-type map:
```javascript
facts.nodeTypeByLine: number[]   // NodeType enum value, or null for blank
```
Built cheaply from a single AST walk. Validators then check:
```javascript
const nodeType = facts.nodeTypeByLine[lineIndex];
if (nodeType === NodeType.FunctionDeclaration) continue;
if (nodeType === NodeType.MacroDeclaration) continue;
if (nodeType === NodeType.IncludeDirective) continue;
```
`shouldSkipFunctionValidationLine` is deleted.

---

## Finding 2 — `extractStandaloneCallName()` is fragile string-level classification

**Lines:** 48–70**

```javascript
function extractStandaloneCallName(lineText) {
    const withoutHash = lineText.trim().replace(/^#\s*/, "");
    if (!withoutHash) return null;

    // Keyword-led lines cannot be standalone calls:
    if (/^(set|if|elseif|while|until|do|case|value|otherwise|return|function|macro|var|...)b/i.test(withoutHash)) {
        return null;
    }
    if (withoutHash.includes("=")) return null;   // assignment — not a standalone call

    const callMatch = withoutHash.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*$/);
    if (!callMatch) return null;
    return callMatch[1];
}
```

This function exists to detect `ExpressionStatement` nodes that are function calls — i.e., lines
where a function is called for its side effect with no assignment target, producing an ACU-FNC-008
("function result ignored") diagnostic.

**What the AST provides:** An `ExpressionStatement` node whose `value` (TokenSpan) starts with
an IDENTIFIER followed by LPAREN is exactly a standalone function call. The expression parser
would confirm this as a `Call` node:
```javascript
{ type: ExprNodeType.Call, callee: Identifier("funcName"), args: [...], ... }
```

**The keyword exclusion list** (`set|if|elseif|while|...`) is attempting to reproduce the
parser's keyword recognition. It is incomplete (missing `ENDDO`, `ENDCASE`, `ENDFUNCTION`,
`BREAK`, `CONTINUE`, etc. — though these don't form callable patterns so it doesn't matter
in practice). More importantly, ELSEIF appears in the list but is not an Architect keyword —
it is an artefact of a pattern that was removed (see memory: ELSEIF removed 2026-04-06).

**The `withoutHash.includes("=")` exclusion** is a blanket heuristic to skip assignment lines.
This is incorrect for lines like `functionCall(a = 1)` where `=` is inside a function argument,
though in practice Architect does not use named arguments.

**Recommended fix (Tier 2):**
Use AST node type to identify ExpressionStatement, then use expression parser to confirm it is
a Call:
```javascript
// With nodeTypeByLine in facts:
if (facts.nodeTypeByLine[lineIndex] !== NodeType.ExpressionStatement) continue;
const stmt = facts.stmtByLine[lineIndex];
const expr = parseExpression(stmt.value);
if (expr?.type !== ExprNodeType.Call) continue;
const standaloneCallName = expr.callee.name;
```
No regex, no incomplete keyword list, no false `=` exclusion.

---

## Finding 3 — `validateCodeSnippet()` regex is the core of the function scanning problem

**Lines:** 428–546**

```javascript
const functionPattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/gi;
const scanCode = stripQuotedStrings(code);
let match;
while ((match = functionPattern.exec(scanCode)) !== null) {
    const functionName = match[1];
    // ...
}
```

This regex scans a quote-masked string for `name(` patterns — a textbook example of parsing
inside a validator. The quote-masking (`stripQuotedStrings`) is itself a manual re-implementation
of what the lexer provides (STRING_LITERAL tokens are already separate from identifiers).

**Why this is hard to fix directly:** The `code` passed to `validateCodeSnippet` may be a
*snippet* extracted from an output line (the part between OICC/CICC delimiters). These
snippets are not full statements — they are expression fragments. The expression parser would
need to handle a TokenSpan that corresponds to a partial expression.

The `functionValidationTargetsByLine` in facts already pre-computes snippets from output lines.
If these snippets were stored as TokenSpans (not raw strings), they could be passed to
`parseExpression`. This is the architectural decision: whether output-line snippets should
be stored as token arrays (already available since the lexer has tokenised the whole document)
or as raw strings (current approach).

**Assessment:** This is the largest single change in the Tier 3 bucket. The regex approach is
not *wrong* — it produces correct results in practice — but it is a maintenance liability. Any
change to function-call syntax in the language would require updating the regex, not just the
lexer/parser.

**Recommended path:** Do not attempt this in isolation. First complete Tier 1 and Tier 2 changes
to build confidence. Then evaluate whether threadding token arrays through the output-snippet
extraction is viable.

---

## Finding 4 — `indexOf` position calculation for standalone call (minor)

**Line:** 175

```javascript
const functionIndex = rawLineText.toUpperCase().indexOf(standaloneCallName.toUpperCase());
```

This finds the first occurrence of the function name in the upper-cased line. If the function
name appears as part of a comment or string earlier on the same line (unusual but possible),
the position is wrong.

With the Tier 2 fix (using expression parser Call nodes), the callee token already carries its
exact `char` position — no `indexOf` needed.

---

## Summary

| Finding | Current approach | Better approach | Tier |
|---|---|---|---|
| `shouldSkipFunctionValidationLine` | Regex on raw line text | `facts.nodeTypeByLine` | 2 |
| `extractStandaloneCallName` | Regex + keyword list | AST `ExpressionStatement` + expression parser Call | 2 |
| `validateCodeSnippet` regex | `functionPattern.exec(scanCode)` | Expression parser Call nodes (requires token-aware snippet extraction) | 3 |
| `indexOf` position | `rawLineText.toUpperCase().indexOf(name)` | Token `char` from callee node | follows from Tier 2 |
