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.
- package/dist/completion/complete.js +13 -4
- package/dist/completion/jinja-slot.d.ts +6 -0
- package/dist/completion/jinja-slot.js +7 -2
- package/dist/minijinja/apply-tags.js +19 -10
- package/dist/qualify/template-provider.d.ts +5 -2
- package/dist/qualify/template-provider.js +5 -2
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|