Skip to content

lint: an include fragment analyzed as a root reports every symbol its includer supplies as undefined #158

Description

@evanbrobertson

An .i file is a fragment: the symbols it uses are frequently declared by a sibling include that the including program pulls in, not by anything the fragment itself references. The CLI never notices, because .i is never a root in the discovery policy. The editor analyzes whatever file is open, so opening an include produces a wave of undefined-symbol errors on code that compiles.

The same code is therefore silent through the gate and noisy in the editor — not because the pipeline differs, but because each client feeds it a different kind of input.

Repro

/* helpers.i — included by the program before the fragment below */
FUNCTION fetch-param RETURNS CHARACTER (INPUT pName AS CHARACTER):
    RETURN "".
END FUNCTION.
/* fragment.i — real logic, no declarations of its own */
sContentType = TRIM(fetch-param("content":U)).
/* program.p — the root */
{helpers.i}
{fragment.i}

oxabl check program.p is clean. oxabl check fragment.i reports undefined symbol fetch-param``, and so does the editor the moment fragment.i is opened.

Scale

Measured on a large real-world ABL codebase kept outside this repo, sampling 40 include files: 24 of 40 carried at least one LINT0001, 709 findings across the sample. Include files outnumber root files there, so the in-editor volume from this exceeds the entire root walk's LINT0001 count by several times. Any codebase that keeps logic in includes — web/CGI-style ABL especially — hits it constantly.

The shape of the fix, and the reason model already has the vocabulary

An unresolved name in a fragment is not absent. It is not knowable from here: the declaring context is whichever program includes this file, and oxabl is not looking at one. That is the distinction UnresolvedReason already draws — External means "we did not look", and every rule skip-lists it, while AbsentFromWorkspace means "we searched and it is not there" and is the only cross-file reason undefined-symbol reports.

So the candidate fix is to analyze a file opened as an .i in a mode where a name that fails locally is External rather than reported, and to say so, rather than to suppress a rule per-symbol. Options considered:

  1. Fragment mode — a root-position .i yields External for names it cannot resolve. Keeps parse, format and every scope-independent rule working while declining to claim what it cannot know. Preferred: it reuses the existing reason model rather than adding a suppression, and it is honest instead of silent.
  2. No diagnostics for .i at all — exactly matches the CLI root policy, but loses all feedback while editing an include, which is a real capability loss where logic lives in includes.
  3. Infer an including context and analyze the fragment through it. Best answers, but a fragment may have many includers with conflicting contexts, and picking one silently invents a scope.
  4. Report at reduced severity — leaves the noise, just quieter.

Whichever is chosen, the acceptance criterion is the asymmetry: the same include should not be clean through check and covered in errors in the editor.

Notes

Surfaced during in-editor dogfood, which is the loop STRATEGY.md says exists to catch trust-eroding false positives that corpus runs cannot — the corpus walk reports zero of these by construction. Pre-existing and independent of cross-file resolution.

Related: #142 (a nested unresolvable include drops its PREPROC007) is the neighbouring include-visibility gap, and #143 (make the root extension set configurable) touches the same root-vs-fragment policy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions