sqllens 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,20 +130,29 @@ function collect(doc, offset, schema) {
130
130
  function templateCompletions(slot, schema) {
131
131
  if (!(schema instanceof DefaultTemplateProvider))
132
132
  return [];
133
- return schema.templateCandidates(slot.callee, slot.argIndex, slot.packageName).map((c) => ({
133
+ return schema.templateCandidates(slot.call, slot.argIndex).map((c) => ({
134
134
  label: c.label,
135
135
  kind: "template",
136
136
  ...(c.detail !== undefined ? { detail: c.detail } : {}),
137
137
  }));
138
138
  }
139
- /** The walk's caret token index: the first default-channel token whose `.start >= offset`; for an
140
- * end-of-input caret that is the EOF sentinel's index (last entry). Mirrors Task 10's tests' caret
141
- * helper. `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended. */
139
+ /** The walk's caret token index. Two rules, in order (anvil 2026-07-15; antlr4-c3's own caret
140
+ * convention):
141
+ * 1. the token being TYPED a word-like token whose span CONTAINS the caret (start < offset <=
142
+ * end). A caret at the end of `ifn` completes `ifn`; it does not mean the slot is filled.
143
+ * Word-like only: punctuation is never partially typed, so `abs(|` keeps rule 2.
144
+ * 2. between tokens — the first default-channel token whose `.start >= offset`; for an
145
+ * end-of-input caret that is the EOF sentinel's index (last entry).
146
+ * `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended.
147
+ * Source order makes one pass sufficient: a containing token starts before any `.start >= offset`
148
+ * token, so rule 1 fires first whenever it applies. */
142
149
  function caretTokenIndex(toks, offset) {
143
150
  for (let i = 0; i < toks.length; i++) {
144
151
  const t = toks[i];
145
152
  if (!t || t.channel !== Token.DEFAULT_CHANNEL)
146
153
  continue;
154
+ if (/^\w/.test(t.text) && t.start < offset && offset <= t.start + t.text.length)
155
+ return i;
147
156
  if (t.start >= offset)
148
157
  return i;
149
158
  }
@@ -1,3 +1,4 @@
1
+ import type { TemplateCall } from "../qualify/template-provider.js";
1
2
  import type { TagNode } from "../minijinja/tag-ast.js";
2
3
  /** Where the caret sits inside a jinja call tag. NEUTRAL, the callee is a bare string; the dbt
3
4
  * meaning of the slot (ref arg0 = a model) is the consumer's to apply. */
@@ -6,6 +7,11 @@ export interface JinjaSlot {
6
7
  callee: string;
7
8
  /** Dotted package before the callee (`dbt_utils` in `dbt_utils.star(...)`). */
8
9
  packageName?: string;
10
+ /** The WHOLE parsed call (issue #37): name, packageParts and every sibling arg's literal value
11
+ * (null where computed) — the same TemplateCall shape every provider method receives. A slot's
12
+ * candidates can depend on the other args (source('raw', '|')'s candidates are the tables OF
13
+ * raw), so the provider callback gets the call, not just the callee name. */
14
+ call: TemplateCall;
9
15
  /** 0-based index of the positional arg the caret is in. The callee-name slot (caret still in the
10
16
  * callee identifier, `{{ my_mac|`) is `-1`. */
11
17
  argIndex: number;
@@ -18,6 +18,7 @@
18
18
  // Reuses the parse: it reads the tags the document already produced, never re-parses.
19
19
  // Total: returns undefined off any jinja completion slot; never throws.
20
20
  // ---------------------------------------------------------------------------
21
+ import { callOf } from "../minijinja/apply-tags.js";
21
22
  /**
22
23
  * The jinja completion slot at `offset`, or undefined when the caret is not in a completable jinja
23
24
  * position. `tags` is `parseTemplated(...).tags` (or `doc.templated.tags`); `text` is the document
@@ -82,7 +83,10 @@ function macroHit(c) {
82
83
  }
83
84
  /** The slot for a caret inside a resolved call: the callee name, or the positional argument. */
84
85
  function slotFromCall(c, text, offset) {
85
- const base = { callee: c.name, ...(c.packageName !== undefined ? { packageName: c.packageName } : {}) };
86
+ // The whole call rides the slot (#37): callOf reads name + literal args off the source text,
87
+ // the same extraction apply-tags feeds the provider everywhere else.
88
+ const call = callOf({ name: c.name, nameSpan: c.nameSpan, args: c.args, ...(c.packageName !== undefined ? { packageName: c.packageName } : {}) }, text);
89
+ const base = { callee: c.name, call, ...(c.packageName !== undefined ? { packageName: c.packageName } : {}) };
86
90
  // Callee-name slot: the caret is still within (or right at the end of) the callee identifier,
87
91
  // before the open paren, the user is typing the macro name itself.
88
92
  if (offset <= c.nameSpan.end) {
@@ -122,7 +126,8 @@ function bareCalleeSlot(tags, text, offset) {
122
126
  const m = /^\s*([A-Za-z_]\w*)$/.exec(before);
123
127
  if (!m)
124
128
  return undefined;
125
- return { callee: m[1], argIndex: -1, prefix: m[1], incomplete: true };
129
+ // No parsed call exists yet (`{{ re`) the slot's call is the bare callee with no args.
130
+ return { callee: m[1], call: { name: m[1], args: [] }, argIndex: -1, prefix: m[1], incomplete: true };
126
131
  }
127
132
  /** Drop a single leading quote from a partial string arg (`'cu` -> `cu`) so the prefix is the value
128
133
  * the consumer filters by. Leaves a non-string arg untouched. */
@@ -17,8 +17,9 @@
17
17
  // TemplateProvider for a call's relation (`provider.relationOf`) and carries no ref/source vocabulary
18
18
  // itself. The NEUTRAL provider answers nothing, so a bare parse leaves calls opaque; a
19
19
  // DbtTemplateProvider names ref/source. Naming is never-wrong: a resolved name comes from the call's
20
- // literal args, and a call whose relation the provider does not resolve keeps its placeholder name and
21
- // stays opaque. We NEVER fabricate a name.
20
+ // literal args, and a call whose relation the provider does not resolve takes the RAW TAG TEXT as its
21
+ // name ({{ ref('m') }} verbatim) and stays opaque. We NEVER fabricate a name — in particular the
22
+ // placeholder fill (scaffolding this library invented) never escapes as one (issue #35).
22
23
  //
23
24
  // The IR is frozen after lower(); this transform REBUILDS with STRUCTURAL SHARING (new objects only on
24
25
  // changed paths, an unchanged subtree keeps its original already-frozen reference) and re-freezes the
@@ -152,7 +153,10 @@ function markTemplateExprs(node, ctx) {
152
153
  // never wrong.
153
154
  // ---------------------------------------------------------------------------
154
155
  /** Match a direct string-literal argument's raw text (no escapes, one token). */
155
- const LITERAL_ARG = /^(['"])([^'"\\]*)\1$/;
156
+ /** A literal argument's value: a quoted string (quote-stripped) or a bare numeric literal —
157
+ * both are the user's own text, never computed. Everything else stays `null` (computed). */
158
+ const LITERAL_ARG = /^(?:(['"])([^'"\\]*)\1|(-?\d+(?:\.\d+)?))$/;
159
+ const literalValueOf = (m) => m[2] ?? m[3];
156
160
  /** The raw text of a span. */
157
161
  function sliceSpan(text, span) {
158
162
  return text.slice(span.start, span.end);
@@ -172,11 +176,11 @@ export function callOf(mc, text) {
172
176
  const kw = KWARG_RE.exec(raw);
173
177
  if (kw) {
174
178
  const m = LITERAL_ARG.exec(kw[2].trim());
175
- kwargs.push({ name: kw[1], value: m ? m[2] : null });
179
+ kwargs.push({ name: kw[1], value: m ? literalValueOf(m) : null });
176
180
  }
177
181
  else {
178
182
  const m = LITERAL_ARG.exec(raw);
179
- args.push(m ? m[2] : null);
183
+ args.push(m ? literalValueOf(m) : null);
180
184
  }
181
185
  }
182
186
  return {
@@ -398,6 +402,11 @@ function transformTableSource(src, ctx) {
398
402
  // where `jjj…` was a fabrication.
399
403
  const aliasTok = src.aliasCst?.start;
400
404
  const base = aliasTok != null && inSpan(aliasTok.start, tag.tagSpan) ? withoutAlias(src) : src;
405
+ // An unresolved source's name is the RAW TAG TEXT — the bytes the user actually wrote. The
406
+ // placeholder fill is scaffolding this library invented so the grammar parses; letting it
407
+ // escape as a relation name (scope sources, lineage dependencies, go-to-def) is fabrication
408
+ // under the never-wrong rule (issue #35, reported by anvil).
409
+ const rawTagName = [ctx.text.slice(tag.tagSpan.start, tag.tagSpan.end)];
401
410
  // NOTE: `template.span` intentionally aliases `tag.tagSpan` BY REFERENCE. freezeIR
402
411
  // therefore also freezes the TagNode.tagSpan object returned in `.tags`, benign
403
412
  // since spans are read-only. Every call marker carries its `call`, the provider key
@@ -406,24 +415,24 @@ function transformTableSource(src, ctx) {
406
415
  const call = callOf(tag, ctx.text);
407
416
  const rel = ctx.provider.relationOf(call);
408
417
  // A call in a FROM slot (ref/source/a TVF-like macro). When the provider resolves its
409
- // relation, carry the resolved name; otherwise keep the placeholder name (never fabricated).
418
+ // relation, carry the resolved name; otherwise the raw tag text (never the fill).
410
419
  // Either way the `call` keeps it consultable, so an unresolved call is not a dead end: a
411
420
  // provider added later resolves it. ref vs source is not stored here, it is call.name.
412
- const named = rel ? { ...base, name: [...rel.nameParts] } : base;
421
+ const named = { ...base, name: rel ? [...rel.nameParts] : rawTagName };
413
422
  const template = { kind: "call", span: tag.tagSpan, call };
414
423
  return attach(ctx, { ...named, template }, tag);
415
424
  }
416
425
  // Non-call expression tag (var / env_var / other) in a FROM slot. A bare `{{ t }}` resolving
417
426
  // through a `{% set t = … %}` single-call RHS carries the resolved relation name (when the
418
427
  // provider resolved it) or the call identity alone; every other case gets the opaque "expr"
419
- // marker, so the placeholder name stops posing as a real table.
428
+ // marker. In every unresolved case the name is the raw tag text, never the fill.
420
429
  const ident = tag.kind === "other" ? bareIdentOf(tag, ctx.text) : undefined;
421
430
  const resolved = ident !== undefined ? ctx.sets.get(ident) : undefined;
422
431
  if (resolved) {
423
- const named = resolved.name ? { ...base, name: [...resolved.name] } : base;
432
+ const named = { ...base, name: resolved.name ? [...resolved.name] : rawTagName };
424
433
  const template = { kind: "call", span: tag.tagSpan, indirect: true, call: resolved.call };
425
434
  return attach(ctx, { ...named, template }, tag);
426
435
  }
427
436
  const template = { kind: "expr", span: tag.tagSpan, opaque: true };
428
- return attach(ctx, { ...base, template }, tag);
437
+ return attach(ctx, { ...base, name: rawTagName, template }, tag);
429
438
  }
@@ -110,8 +110,11 @@ export declare class DefaultTemplateProvider implements SchemaProvider {
110
110
  * the table names). `argIndex` is -1 when the caret is still in the callee name itself, so a host
111
111
  * can answer the macro/callee names it knows. `packageName` is the dotted package (`dbt_utils` in
112
112
  * `dbt_utils.star(...)`). The NEUTRAL provider knows no vocabulary and offers none; a host answers
113
- * from its catalog. `completeAt` reads this when the caret is inside a jinja tag. */
114
- templateCandidates(_callee: string, _argIndex: number, _packageName?: string): TemplateCandidate[];
113
+ * from its catalog. `completeAt` reads this when the caret is inside a jinja tag. The WHOLE
114
+ * parsed call comes along (issue #37) a slot's candidates can depend on the sibling args:
115
+ * `source('raw', '|')`'s candidates are the tables OF the source named in `call.args[0]`.
116
+ * `argIndex` is the positional arg the caret is in (`-1` = the callee-name slot). */
117
+ templateCandidates(_call: TemplateCall, _argIndex: number): TemplateCandidate[];
115
118
  /**
116
119
  * Everything known about `call`, composed from the granular methods. Field precedence for the
117
120
  * shape (channel-agreed): an EXPLICIT `shapeOf` answer always wins; absent, derived
@@ -137,8 +137,11 @@ export class DefaultTemplateProvider {
137
137
  * the table names). `argIndex` is -1 when the caret is still in the callee name itself, so a host
138
138
  * can answer the macro/callee names it knows. `packageName` is the dotted package (`dbt_utils` in
139
139
  * `dbt_utils.star(...)`). The NEUTRAL provider knows no vocabulary and offers none; a host answers
140
- * from its catalog. `completeAt` reads this when the caret is inside a jinja tag. */
141
- templateCandidates(_callee, _argIndex, _packageName) {
140
+ * from its catalog. `completeAt` reads this when the caret is inside a jinja tag. The WHOLE
141
+ * parsed call comes along (issue #37) — a slot's candidates can depend on the sibling args:
142
+ * `source('raw', '|')`'s candidates are the tables OF the source named in `call.args[0]`.
143
+ * `argIndex` is the positional arg the caret is in (`-1` = the callee-name slot). */
144
+ templateCandidates(_call, _argIndex) {
142
145
  return [];
143
146
  }
144
147
  // --- The composed entry the ENGINE consults (rarely overridden wholesale). ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqllens",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A TypeScript SQL parser and static analyzer: parse, resolve names, infer types, and trace column lineage across many SQL dialects (Databricks, T-SQL, Snowflake, BigQuery, Redshift, PostgreSQL, DuckDB, Trino, SQLite, MySQL).",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",