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:
- 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.
- 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.
- 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.
- 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.
An
.ifile 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.iis never a root in the discovery policy. The editor analyzes whatever file is open, so opening an include produces a wave ofundefined-symbolerrors 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
oxabl check program.pis clean.oxabl check fragment.ireportsundefined symbolfetch-param``, and so does the editor the momentfragment.iis 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
UnresolvedReasonalready draws —Externalmeans "we did not look", and every rule skip-lists it, whileAbsentFromWorkspacemeans "we searched and it is not there" and is the only cross-file reasonundefined-symbolreports.So the candidate fix is to analyze a file opened as an
.iin a mode where a name that fails locally isExternalrather than reported, and to say so, rather than to suppress a rule per-symbol. Options considered:.iyieldsExternalfor 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..iat 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.Whichever is chosen, the acceptance criterion is the asymmetry: the same include should not be clean through
checkand covered in errors in the editor.Notes
Surfaced during in-editor dogfood, which is the loop
STRATEGY.mdsays 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.