Bug: Copybook members whose name starts with $ are not found ("not found" hover tooltip)
Summary
When a /copy or /include directive references a source member whose name begins with the IBM i variant character $ (dollar sign), the RPGLE language server shows the tooltip "(not found)" and does not resolve the member — even though the member exists on the system.
Example:
/copy qcpysrc,$ADCONST ← shown as "(not found)"
/copy qcpysrc,$RPGPSDS ← shown as "(not found)"
/copy qcpysrc,AA0100S ← found correctly
Environment
| Item |
Version |
| Extension |
halcyontechltd.code-for-ibmi v3.0.7 |
| RPGLE extension |
halcyontechltd.vscode-rpgle v0.33.4 |
| VS Code |
1.x |
| IBM i OS |
V7Rx |
Root Cause
File: src/api/Tools.ts
Function: sanitizeObjNamesForPase
The function sanitizes IBM i object/member names for use inside PASE shell commands.
Currently it only handles names beginning with # (another IBM i variant character) by wrapping them in double quotes:
// Current (buggy) implementation
export function sanitizeObjNamesForPase(names: string[]): string[] {
return names.map(name => name.startsWith('#') ? `"${name}"` : name);
}
Names starting with $ pass through unchanged.
How the bug manifests
IBMiContent.memberResolve() calls Tools.qualifyPath() with noEscape = true, which relies entirely on sanitizeObjNamesForPase for shell-safety:
// src/api/IBMiContent.ts — memberResolve()
const pathList = files
.map(file => Tools.qualifyPath(
inAmerican(file.library),
inAmerican(file.name),
inAmerican(member), // e.g. "$ADCONST"
asp,
true // noEscape = true → no escapePath() fallback!
))
.join(` `)
.toUpperCase();
const command = `for f in ${pathList}; do if [ -f $f ]; then echo $f; break; fi; done`;
For member $ADCONST (library MYLIB, source file QCPYSRC) the generated shell command is:
for f in /QSYS.LIB/MYLIB.LIB/QCPYSRC.FILE/$ADCONST.MBR; do if [ -f $f ]; then echo $f; break; fi; done
The PASE shell expands $ADCONST as a shell variable.
Because $ADCONST is undefined, it expands to an empty string.
The path becomes /QSYS.LIB/MYLIB.LIB/QCPYSRC.FILE/.MBR, which does not exist.
memberResolve returns undefined → the language server shows "not found".
Why does # work but $ does not?
# has no special meaning inside double-quoted shell strings, so "#MEMBER" expands correctly to #MEMBER.
$ is special inside double-quoted strings — the shell still performs variable expansion.
Single quotes or a backslash escape is required to suppress $ expansion.
Suggested Fix
File: src/api/Tools.ts
Extend sanitizeObjNamesForPase to also protect names starting with $
using a backslash escape (which works correctly in an unquoted shell context):
// Suggested fix
export function sanitizeObjNamesForPase(names: string[]): string[] {
return names.map(name => {
if (name.startsWith('#')) return `"${name}"`; // existing behaviour
if (name.startsWith('$')) return `\\${name}`; // NEW: escape $ for PASE shell
return name;
});
}
This produces:
for f in /QSYS.LIB/MYLIB.LIB/QCPYSRC.FILE/\$ADCONST.MBR; do ...
The shell treats \$ as a literal dollar sign, so the path resolves correctly.
Alternatively, use single quotes around the component
(single quotes prevent all shell expansion and are safe since IBM i member names cannot contain single quotes):
if (name.startsWith('$')) return `'${name}'`;
This produces:
for f in /QSYS.LIB/MYLIB.LIB/QCPYSRC.FILE/'$ADCONST'.MBR; do ...
Both approaches are equivalent; the backslash variant is slightly more consistent with how Tools.escapePath() already handles $.
Additional notes
- The same
sanitizeObjNamesForPase gap likely affects @-prefixed member names (the third IBM i variant character), though @ is not special in POSIX shell, so it does not currently cause a visible failure.
objectResolve() in IBMiContent.ts constructs its shell path by calling sysNameInAmerican() directly — without going through sanitizeObjNamesForPase — so it would also fail for $-prefixed object names. That may be worth a follow-up fix.
Tools.escapePath() already escapes $ correctly (see src/api/Tools.ts), but it is bypassed here because noEscape = true.
Steps to Reproduce
- Open an RPGLE source member on IBM i that contains:
- Hover over the directive.
- Expected: tooltip shows the resolved member path, e.g.
`MYLIB/QCPYSRC/$ADCONST` (found)
- Actual: tooltip shows
qcpysrc,$ADCONST (not found)
References
src/api/Tools.ts — sanitizeObjNamesForPase, qualifyPath, escapePath
src/api/IBMiContent.ts — memberResolve
Bug: Copybook members whose name starts with
$are not found ("not found" hover tooltip)Summary
When a
/copyor/includedirective references a source member whose name begins with the IBM i variant character$(dollar sign), the RPGLE language server shows the tooltip "(not found)" and does not resolve the member — even though the member exists on the system.Example:
Environment
halcyontechltd.code-for-ibmiv3.0.7halcyontechltd.vscode-rpglev0.33.4Root Cause
File:
src/api/Tools.tsFunction:
sanitizeObjNamesForPaseThe function sanitizes IBM i object/member names for use inside PASE shell commands.
Currently it only handles names beginning with
#(another IBM i variant character) by wrapping them in double quotes:Names starting with
$pass through unchanged.How the bug manifests
IBMiContent.memberResolve()callsTools.qualifyPath()withnoEscape = true, which relies entirely onsanitizeObjNamesForPasefor shell-safety:For member
$ADCONST(libraryMYLIB, source fileQCPYSRC) the generated shell command is:The PASE shell expands
$ADCONSTas a shell variable.Because
$ADCONSTis undefined, it expands to an empty string.The path becomes
/QSYS.LIB/MYLIB.LIB/QCPYSRC.FILE/.MBR, which does not exist.memberResolvereturnsundefined→ the language server shows "not found".Suggested Fix
File:
src/api/Tools.tsExtend
sanitizeObjNamesForPaseto also protect names starting with$using a backslash escape (which works correctly in an unquoted shell context):
This produces:
The shell treats
\$as a literal dollar sign, so the path resolves correctly.Alternatively, use single quotes around the component
(single quotes prevent all shell expansion and are safe since IBM i member names cannot contain single quotes):
This produces:
Both approaches are equivalent; the backslash variant is slightly more consistent with how
Tools.escapePath()already handles$.Additional notes
sanitizeObjNamesForPasegap likely affects@-prefixed member names (the third IBM i variant character), though@is not special in POSIX shell, so it does not currently cause a visible failure.objectResolve()inIBMiContent.tsconstructs its shell path by callingsysNameInAmerican()directly — without going throughsanitizeObjNamesForPase— so it would also fail for$-prefixed object names. That may be worth a follow-up fix.Tools.escapePath()already escapes$correctly (seesrc/api/Tools.ts), but it is bypassed here becausenoEscape = true.Steps to Reproduce
`MYLIB/QCPYSRC/$ADCONST` (found)qcpysrc,$ADCONST (not found)References
src/api/Tools.ts—sanitizeObjNamesForPase,qualifyPath,escapePathsrc/api/IBMiContent.ts—memberResolve