|
|
@@ -7,10 +7,13 @@
|
|
|
|
|
|
import * as fs from 'fs';
|
|
|
import * as path from 'path';
|
|
|
-import { lex } from './lexer';
|
|
|
+import { lex, Token } from './lexer';
|
|
|
import { M2Import, M2Symbol, M2Unit, parseUnitText, rangeContains } from './parser';
|
|
|
import { gm2LibraryDirs, projectSearchDirs } from './defpaths';
|
|
|
-import { findRecordField } from './types';
|
|
|
+import {
|
|
|
+ findFieldInType, findRecordField, resolveDeclaredType, resolveType, unitOfFile,
|
|
|
+} from './types';
|
|
|
+import type { M2Type } from './types';
|
|
|
|
|
|
export interface M2Location {
|
|
|
filePath: string;
|
|
|
@@ -293,6 +296,211 @@ export function resolveName(
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+// ---- WITH scopes (bare field names in WITH bodies) ----
|
|
|
+
|
|
|
+const WITH_OPENERS = new Set([
|
|
|
+ 'BEGIN', 'IF', 'CASE', 'LOOP', 'WHILE', 'FOR', 'WITH', 'RECORD', 'MODULE',
|
|
|
+]);
|
|
|
+
|
|
|
+interface WithBlock {
|
|
|
+ guardFrom: number;
|
|
|
+ guardTo: number;
|
|
|
+ bodyFromIdx: number;
|
|
|
+ endIdx: number;
|
|
|
+}
|
|
|
+
|
|
|
+/** WITH blocks from tokens (twin of analyse.withRegionsOf, keeping spans).
|
|
|
+ *
|
|
|
+ * Guards are short designators, so the `DO` search stays bounded like
|
|
|
+ * the analyser's; bodies nest with the usual opener counting. Anything
|
|
|
+ * unparseable yields no block and stays silent downstream. */
|
|
|
+function withBlocksOf(tokens: Token[]): WithBlock[] {
|
|
|
+ const cached = withBlockCache.get(tokens);
|
|
|
+ if (cached) return cached;
|
|
|
+ const out: WithBlock[] = [];
|
|
|
+ for (let k = 0; k < tokens.length; k++) {
|
|
|
+ const t = tokens[k];
|
|
|
+ if (t.kind !== 'keyword' || t.text !== 'WITH') continue;
|
|
|
+ let doIdx = -1;
|
|
|
+ for (let j = k + 1; j < Math.min(k + 13, tokens.length); j++) {
|
|
|
+ const u = tokens[j];
|
|
|
+ if (u.kind === 'keyword' && u.text === 'DO') { doIdx = j; break; }
|
|
|
+ if ((u.kind === 'symbol' && u.text === ';') ||
|
|
|
+ (u.kind === 'keyword' && (u.text === 'BEGIN' || u.text === 'END'))) break;
|
|
|
+ }
|
|
|
+ if (doIdx < 0 || doIdx + 1 >= tokens.length) continue;
|
|
|
+ let depth = 1;
|
|
|
+ for (let m = doIdx + 1; m < tokens.length; m++) {
|
|
|
+ const u = tokens[m];
|
|
|
+ if (u.kind === 'keyword' && WITH_OPENERS.has(u.text)) depth++;
|
|
|
+ else if (u.kind === 'keyword' && u.text === 'END') {
|
|
|
+ depth--;
|
|
|
+ if (depth === 0) {
|
|
|
+ out.push({ guardFrom: k + 1, guardTo: doIdx - 1, bodyFromIdx: doIdx + 1, endIdx: m });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ withBlockCache.set(tokens, out);
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+const withBlockCache = new WeakMap<Token[], WithBlock[]>();
|
|
|
+
|
|
|
+function posInBody(b: WithBlock, tokens: Token[], line: number, ch: number): boolean {
|
|
|
+ const s = tokens[b.bodyFromIdx];
|
|
|
+ const e = tokens[b.endIdx];
|
|
|
+ if (!s || !e) return false;
|
|
|
+ const afterStart = line > s.line || (line === s.line && ch >= s.ch);
|
|
|
+ const beforeEnd = line < e.line || (line === e.line && ch < e.ch);
|
|
|
+ return afterStart && beforeEnd;
|
|
|
+}
|
|
|
+
|
|
|
+/** WITH-aware name resolution for hover/definition/references/rename.
|
|
|
+ *
|
|
|
+ * Bare field names inside WITH bodies resolve to the guard record's
|
|
|
+ * field declarations (innermost guard first, mirroring the checker's
|
|
|
+ * scope stack); everything else falls back to `resolveName`, which is
|
|
|
+ * left untouched. Qualified heads consult the WITH stack too, so
|
|
|
+ * `inner.y` hovers `y`. Never throws. */
|
|
|
+export function resolveNameWith(
|
|
|
+ unit: M2Unit, docDir: string, tokens: Token[],
|
|
|
+ line: number, ch: number, qualifier: string | null, name: string,
|
|
|
+): Resolved | null {
|
|
|
+ if (qualifier) {
|
|
|
+ const head = resolveHeadWith(unit, docDir, tokens, line, ch, qualifier, 0);
|
|
|
+ if (head && (head.sym.kind === 'variable' || head.sym.kind === 'parameter' ||
|
|
|
+ head.sym.kind === 'constant' || head.sym.kind === 'field')) {
|
|
|
+ const host = head.filePath === unit.filePath ? unit : unitOfFile(head.filePath) ?? unit;
|
|
|
+ const dir = head.filePath === unit.filePath ? docDir : path.dirname(head.filePath);
|
|
|
+ const f = findRecordField(host, dir, head.sym, name, line, ch);
|
|
|
+ if (f) return { sym: f.sym, filePath: f.unit.filePath };
|
|
|
+ }
|
|
|
+ return resolveName(unit, docDir, line, ch, qualifier, name);
|
|
|
+ }
|
|
|
+ return resolveHeadWith(unit, docDir, tokens, line, ch, name, 0)
|
|
|
+ ?? resolveName(unit, docDir, line, ch, null, name);
|
|
|
+}
|
|
|
+
|
|
|
+/** A bare name through enclosing WITH guards (innermost first). */
|
|
|
+function resolveHeadWith(
|
|
|
+ unit: M2Unit, docDir: string, tokens: Token[],
|
|
|
+ line: number, ch: number, name: string, depth: number,
|
|
|
+): Resolved | null {
|
|
|
+ if (depth > 8) return null;
|
|
|
+ const blocks = withBlocksOf(tokens)
|
|
|
+ .filter(b => posInBody(b, tokens, line, ch))
|
|
|
+ .sort((a, b) => (a.endIdx - a.bodyFromIdx) - (b.endIdx - b.bodyFromIdx));
|
|
|
+ for (const b of blocks) {
|
|
|
+ const gt = guardRecord(unit, docDir, tokens, b, depth);
|
|
|
+ if (!gt) continue;
|
|
|
+ const f = findFieldInType(unit, docDir, gt, name, line, ch);
|
|
|
+ if (f) return { sym: f.sym, filePath: f.unit.filePath };
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+/** Record type denoted by a WITH guard, or null when unknowable.
|
|
|
+ *
|
|
|
+ * Guards are restricted designators (`rec`, `p^`, `a[i]`, `r.f`);
|
|
|
+ * gm2 rejects call results there, so anything else bails silently.
|
|
|
+ * Nested guards resolve through outer blocks via `resolveHeadWith`. */
|
|
|
+function guardRecord(
|
|
|
+ unit: M2Unit, docDir: string, tokens: Token[], b: WithBlock, depth: number,
|
|
|
+): M2Type | null {
|
|
|
+ const ctx = { unit, docDir };
|
|
|
+ let k = b.guardFrom;
|
|
|
+ while (k <= b.guardTo && tokens[k].kind !== 'ident') k++;
|
|
|
+ if (k > b.guardTo) return null;
|
|
|
+ const ht = tokens[k];
|
|
|
+ const r = resolveHeadWith(unit, docDir, tokens, ht.line, ht.ch, ht.text, depth + 1)
|
|
|
+ ?? resolveName(unit, docDir, ht.line, ht.ch, null, ht.text);
|
|
|
+ if (!r) return null;
|
|
|
+ let t = declaredTypeOf(ctx, r);
|
|
|
+ if (!t) return null;
|
|
|
+ k++;
|
|
|
+ while (k <= b.guardTo) {
|
|
|
+ const tok = tokens[k];
|
|
|
+ if (tok.text === '^') {
|
|
|
+ t = derefType(ctx, t, tok.line, tok.ch);
|
|
|
+ if (!t) return null;
|
|
|
+ k++;
|
|
|
+ } else if (tok.text === '.') {
|
|
|
+ const nm = tokens[k + 1];
|
|
|
+ if (!nm || nm.kind !== 'ident') return null;
|
|
|
+ t = fieldTypeOf(ctx, t, nm.text, nm.line, nm.ch);
|
|
|
+ if (!t) return null;
|
|
|
+ k += 2;
|
|
|
+ } else if (tok.text === '[') {
|
|
|
+ const close = skipBalanced(tokens, k, '[', ']');
|
|
|
+ if (close < 0 || close > b.guardTo) return null;
|
|
|
+ t = elementTypeOf(ctx, t, tok.line, tok.ch);
|
|
|
+ if (!t) return null;
|
|
|
+ k = close + 1;
|
|
|
+ } else {
|
|
|
+ k++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const base = t.kind === 'subrange' ? t.base : t;
|
|
|
+ return base.kind === 'record' ? t : null;
|
|
|
+}
|
|
|
+
|
|
|
+function declaredTypeOf(
|
|
|
+ ctx: { unit: M2Unit; docDir: string }, r: Resolved,
|
|
|
+): M2Type | null {
|
|
|
+ const k = r.sym.kind;
|
|
|
+ if (k !== 'variable' && k !== 'parameter' && k !== 'constant' && k !== 'field') return null;
|
|
|
+ const host = r.filePath === ctx.unit.filePath ? ctx.unit : unitOfFile(r.filePath);
|
|
|
+ if (!host) return null;
|
|
|
+ const dir = r.filePath === ctx.unit.filePath ? ctx.docDir : path.dirname(r.filePath);
|
|
|
+ const t = resolveDeclaredType(
|
|
|
+ r.sym, host, dir, r.sym.nameRange.startLine, r.sym.nameRange.startCh);
|
|
|
+ return t.kind === 'unknown' ? null : t;
|
|
|
+}
|
|
|
+
|
|
|
+function derefType(
|
|
|
+ ctx: { unit: M2Unit; docDir: string }, t: M2Type, line: number, ch: number,
|
|
|
+): M2Type | null {
|
|
|
+ const r = resolveType(t, ctx.unit, ctx.docDir, line, ch);
|
|
|
+ if (r.kind !== 'pointer') return null;
|
|
|
+ const tg = resolveType(r.target, ctx.unit, ctx.docDir, line, ch);
|
|
|
+ return tg.kind === 'unknown' ? null : tg;
|
|
|
+}
|
|
|
+
|
|
|
+function elementTypeOf(
|
|
|
+ ctx: { unit: M2Unit; docDir: string }, t: M2Type, line: number, ch: number,
|
|
|
+): M2Type | null {
|
|
|
+ const r = resolveType(t, ctx.unit, ctx.docDir, line, ch);
|
|
|
+ if (r.kind !== 'array') return null;
|
|
|
+ const el = resolveType(r.element, ctx.unit, ctx.docDir, line, ch);
|
|
|
+ return el.kind === 'unknown' ? null : el;
|
|
|
+}
|
|
|
+
|
|
|
+function fieldTypeOf(
|
|
|
+ ctx: { unit: M2Unit; docDir: string }, t: M2Type,
|
|
|
+ name: string, line: number, ch: number,
|
|
|
+): M2Type | null {
|
|
|
+ const r = resolveType(t, ctx.unit, ctx.docDir, line, ch);
|
|
|
+ if (r.kind !== 'record') return null;
|
|
|
+ const f = findFieldInType(ctx.unit, ctx.docDir, r, name, line, ch);
|
|
|
+ if (!f) return null;
|
|
|
+ const ft = resolveDeclaredType(f.sym, f.unit, f.dir, line, ch);
|
|
|
+ return ft.kind === 'unknown' ? null : ft;
|
|
|
+}
|
|
|
+
|
|
|
+function skipBalanced(tokens: Token[], from: number, open: string, close: string): number {
|
|
|
+ let depth = 0;
|
|
|
+ for (let k = from; k < tokens.length; k++) {
|
|
|
+ if (tokens[k].text === open) depth++;
|
|
|
+ else if (tokens[k].text === close) {
|
|
|
+ depth--;
|
|
|
+ if (depth === 0) return k;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+}
|
|
|
+
|
|
|
/** Exported symbols of a module (for `Module.` completion). */
|
|
|
export function moduleExports(docDir: string, moduleName: string): M2Symbol[] {
|
|
|
const def = loadDefModule(docDir, moduleName);
|
|
|
@@ -342,7 +550,11 @@ export interface LocatedOccurrence extends Occurrence {
|
|
|
|
|
|
/** Every identifier token in the text with its qualifier (`A` in `A.B`). */
|
|
|
export function identifierOccurrences(text: string): Occurrence[] {
|
|
|
- const tokens = lex(text);
|
|
|
+ return identifierOccurrencesIn(lex(text));
|
|
|
+}
|
|
|
+
|
|
|
+/** Every identifier token in a pre-lexed stream with its qualifier (`A` in `A.B`). */
|
|
|
+export function identifierOccurrencesIn(tokens: Token[]): Occurrence[] {
|
|
|
const out: Occurrence[] = [];
|
|
|
for (let k = 0; k < tokens.length; k++) {
|
|
|
const t = tokens[k];
|
|
|
@@ -369,13 +581,28 @@ export function findReferencesInText(
|
|
|
decl: Resolved, includeDeclaration = true,
|
|
|
): LocatedOccurrence[] {
|
|
|
const unit = parseUnitText(text, filePath);
|
|
|
+ const tokens = lex(text);
|
|
|
+ // Declaration sites belong to their own symbol: a shadowing
|
|
|
+ // declaration (e.g. a field) must not count as a use of the symbol
|
|
|
+ // it shadows, which scope resolution would otherwise return.
|
|
|
+ const declared = new Set<string>();
|
|
|
+ for (const { sym } of flattenUnit(unit)) {
|
|
|
+ if (sym.name) declared.add(`${sym.nameRange.startLine}:${sym.nameRange.startCh}`);
|
|
|
+ }
|
|
|
+ const declKey = `${decl.sym.nameRange.startLine}:${decl.sym.nameRange.startCh}`;
|
|
|
const out: LocatedOccurrence[] = [];
|
|
|
- for (const occ of identifierOccurrences(text)) {
|
|
|
+ for (const occ of identifierOccurrencesIn(tokens)) {
|
|
|
if (occ.name !== decl.sym.name) continue;
|
|
|
- const resolved = resolveName(unit, docDir, occ.line, occ.ch, occ.qualifier, occ.name);
|
|
|
+ const key = `${occ.line}:${occ.ch}`;
|
|
|
+ // The declaration occurrence itself (field declarations never
|
|
|
+ // resolve by name, so match it by position).
|
|
|
+ if (filePath === decl.filePath && key === declKey) {
|
|
|
+ if (includeDeclaration) out.push({ ...occ, filePath });
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (declared.has(key)) continue;
|
|
|
+ const resolved = resolveNameWith(unit, docDir, tokens, occ.line, occ.ch, occ.qualifier, occ.name);
|
|
|
if (!resolved || !sameDecl(resolved, decl)) continue;
|
|
|
- if (!includeDeclaration && filePath === decl.filePath &&
|
|
|
- occ.line === decl.sym.nameRange.startLine && occ.ch === decl.sym.nameRange.startCh) continue;
|
|
|
out.push({ ...occ, filePath });
|
|
|
}
|
|
|
return out;
|