codefori / codefori/vscode-ibmi
Copybook members whose name starts with `$` are not found ("not found" hover tooltip)
- Dominant language
- TypeScript
- Stars
- 418
- Forks
- 160
- Avg merge
- 4d 19h
- Merged PRs (30d)
- 12
Description
# 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:**
```rpgle
/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:
```typescript
// 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:
```typescript
// 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:
```bash
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):
```typescript
// 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:
```bash
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):
```typescript
if (name.startsWith('$')) return `'${name}'`;
```
This produces:
```bash
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
1. Open an RPGLE source member on IBM i that contains:
```rpgle
/copy qcpysrc,$ADCONST
```
2. Hover over the directive.
3. **Expected:** tooltip shows the resolved member path, e.g. `` `MYLIB/QCPYSRC/$ADCONST` (found) ``
4. **Actual:** tooltip shows `qcpysrc,$ADCONST (not found)`
---
## References
* `src/api/Tools.ts` — `sanitizeObjNamesForPase`, `qualifyPath`, `escapePath`
* `src/api/IBMiContent.ts` — `memberResolve`
Contributor guide
Assessment
This issue has not been assessed yet.