From adc1704d4b3ad3558764fdf32ca395af3f1908a5 Mon Sep 17 00:00:00 2001
From: Puma7 <88577358+Puma7@users.noreply.github.com>
Date: Sun, 11 Oct 2026 21:45:12 +0200
Subject: [PATCH 1/2] feat(liquid): index capture declarations and document
dialect boundaries
---
CHANGELOG.md | 4 ++
README.md | 19 +++++++-
__tests__/liquid-capture-coverage.test.ts | 59 +++++++++++++++++++++++
src/extraction/liquid-extractor.ts | 10 ++--
src/mcp/server-instructions.ts | 1 +
5 files changed, 89 insertions(+), 4 deletions(-)
create mode 100644 __tests__/liquid-capture-coverage.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d6e834999..e50a1d7555 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### New Features
+
+- Liquid capture variables are now searchable in plain templates and multiline Liquid tags, and the documented coverage distinguishes standard Liquid declarations from Shopify dependencies and unsupported host features. (#2498)
+
### Fixes
- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
diff --git a/README.md b/README.md
index ae3750c3ba..90d4b53e05 100644
--- a/README.md
+++ b/README.md
@@ -830,7 +830,7 @@ is written):
| Svelte | `.svelte` | Full support (script extraction, Svelte 5 runes, SvelteKit routes) |
| Vue | `.vue` | Full support (script + script-setup extraction, Nuxt page/API/middleware routes) |
| Astro | `.astro` | Full support (frontmatter + script extraction, template component/call references, `src/pages/` routes) |
-| Liquid | `.liquid` | Full support |
+| Liquid | `.liquid` | Partial support (assign/capture variables, literal Shopify snippet/section references and schema; [dialect boundaries](#liquid-coverage)) |
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) |
@@ -843,6 +843,23 @@ is written):
| Terraform / OpenTofu | `.tf`, `.tfvars`, `.tofu` | Full support (resources, data sources, modules, variables, outputs, providers incl. aliases, `locals`; `var.`/`local.`/`module.`/resource references with Terraform's per-directory scoping enforced; module calls bridged across the boundary — inputs to the child module's variables, `module.M.out` to the child's output, `source` to the module's files; cloudposse/atmos `remote-state` cross-component wiring when the component is statically named; `provider = aws.east` selections resolved up the module tree; `moved`/`import`/`removed`/`check` block references; `.tfvars` assignments linked to the variables they set) |
| Nix | `.nix` | Full support (functions with simple/destructured/curried params, `let`/attrset bindings, `inherit`, `import ./path` file edges — `./dir` resolving through `default.nix` — plus NixOS module `imports = [ ./x.nix ]` lists and `callPackage ./pkg.nix` file edges; call edges; module-system option wiring — a config write like `launchd.user.agents.x = { ... }` links to the module declaring `options.launchd.user.agents`, so option flows trace across modules) |
+### Liquid coverage
+
+Liquid support is a structural subset, not a complete interpreter or support for every host's extensions. The extractor applies to indexed `.liquid` files; `capture` and `assign` definitions do not require a Shopify directory layout.
+
+| Area | Indexed now | Boundary |
+|---|---|---|
+| Standard Liquid variables | Simple `assign` and `capture` declarations, including whitespace control and bare tags inside a multiline `liquid` tag | No evaluation of captured strings, expression dataflow or variable-read edges; other declaration tags are not covered |
+| Literal template dependencies | Quoted `render`/`include` names become Shopify snippet paths; quoted `section` names become Shopify section paths | These paths describe Shopify conventions, not a generic include loader; variable/computed targets remain unresolved |
+| Shopify schema and JSON | Schema constants and section-type references from the supported JSON template inputs | Editor JSON comments, section groups and schema comment/raw handling are tracked separately in [#2494](https://github.com/colbymchenry/codegraph/issues/2494) / [PR #2496](https://github.com/colbymchenry/codegraph/pull/2496) |
+| Jekyll | Standard declarations in indexed `.liquid` files | Unquoted `include`, `include_relative`, `_includes` lookup and Liquid inside HTML/Markdown/front matter are not supported as Jekyll dependencies |
+| Embedded code and assets | Standalone supported source files are indexed independently | No delegation of inline JavaScript/CSS in `.liquid`; no `asset_url` JS/CSS dependency edges or CSS language registration |
+| Runtime host data | No runtime evaluation | No consumer edges for settings, translations, dynamic block names or Shopify `content_for` |
+
+An absent Liquid dependency edge is not evidence that a template or asset is unused. A file being present in the index does not mean all code embedded in it was parsed. This first coverage extension implements standard `capture` declarations; the remaining host-specific work stays open in [#2498](https://github.com/colbymchenry/codegraph/issues/2498).
+
+The distinction follows the [standard Liquid variable tags](https://shopify.github.io/liquid/tags/variable/) and [Jekyll's own include rules](https://jekyllrb.com/docs/includes/); host extensions need separate routing and resolution, rather than guessing one host's paths for all Liquid projects.
+
## Measured cross-file coverage
Impact and blast-radius queries are only as good as the dependency graph behind them, so coverage is measured rather than asserted. **Fair coverage** = the share of symbol-bearing source files that have at least one *resolved cross-file dependent* — something that imports, calls, references, or (through a framework convention) routes to them — on a real-world benchmark repo per language. The residual is always a genuine static-analysis frontier (runtime dynamic dispatch, reflection / DI containers, framework-convention entry points, vendored third-party code), never hidden by gaming the denominator.
diff --git a/__tests__/liquid-capture-coverage.test.ts b/__tests__/liquid-capture-coverage.test.ts
new file mode 100644
index 0000000000..fddc919238
--- /dev/null
+++ b/__tests__/liquid-capture-coverage.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from 'vitest';
+import { LiquidExtractor } from '../src/extraction/liquid-extractor';
+
+const extract = (source: string, file = 'plain/template.liquid') => new LiquidExtractor(file, source).extract();
+describe('standard Liquid capture declarations', () => {
+ it.each([
+ ['{% capture title %}Hello{% endcapture %}', 'title'],
+ ['{%- capture title -%}Hello{%- endcapture -%}', 'title'],
+ ['{% capture title_text %}Hello{% endcapture %}', 'title_text'],
+ ['{% capture title-text %}Hello{% endcapture %}', 'title-text'],
+ ['{% liquid\n capture title\n echo "Hello"\n endcapture\n%}', 'title'],
+ ['{% liquid\r\n capture title\r\n echo "Hello"\r\n endcapture\r\n%}', 'title'],
+ ['{% liquid\n capture title # a declaration\n echo "Hello"\n endcapture\n%}', 'title'],
+ ])('indexes a variable and file containment: %s', (source, name) => {
+ const result = extract(source);
+ const variable = result.nodes.find(n => n.kind === 'variable');
+ expect(variable?.name).toBe(name);
+ expect(result.edges).toContainEqual(expect.objectContaining({ target: variable!.id, kind: 'contains' }));
+ expect(result.errors).toEqual([]);
+ });
+
+ it.each(['plain/template.liquid', '_includes/card.liquid', 'sections/card.liquid'])('does not require Shopify directories: %s', file => {
+ expect(extract('{% capture title %}Hello{% endcapture %}', file).nodes.some(n => n.name === 'title')).toBe(true);
+ });
+
+ it.each([
+ '{% comment %}{% capture fake %}x{% endcapture %}{% endcomment %}',
+ '{% raw %}{% capture fake %}x{% endcapture %}{% endraw %}',
+ '{% # capture fake %}',
+ '{% liquid\n # capture fake\n%}',
+ '{% liquid\n comment\n capture fake\n endcapture\n endcomment\n%}',
+ '{% liquid\n raw\n capture fake\n endcapture\n endraw\n%}',
+ '{% capture fake extra %}x{% endcapture %}',
+ '{% liquid\n capture fake extra\n%}',
+ '{% assign text = "capture fake" %}',
+ ])('ignores non-executing or invalid declarations: %s', source => {
+ expect(extract(source).nodes.some(n => n.name === 'fake')).toBe(false);
+ });
+
+ it('retains declaration order, positions and identity on one Unicode line', () => {
+ const source = 'é😀 {% assign title = 1 %}{% capture title %}one{% endcapture %}{% capture title %}two{% endcapture %}';
+ const variables = extract(source).nodes.filter(n => n.kind === 'variable');
+ expect(variables).toHaveLength(3);
+ expect(variables.map(n => n.startColumn)).toEqual([source.indexOf('{% assign'), source.indexOf('{% capture'), source.lastIndexOf('{% capture')]);
+ expect(new Set(variables.map(n => n.id)).size).toBe(3);
+ });
+
+ it('retains nested executable assigns and render references', () => {
+ const result = extract('{% capture title %}{% assign inner = 1 %}{% render "card" %}{% endcapture %}');
+ expect(result.nodes.filter(n => n.kind === 'variable').map(n => n.name)).toEqual(['title', 'inner']);
+ expect(result.unresolvedReferences).toContainEqual(expect.objectContaining({ referenceName: 'snippets/card.liquid' }));
+ });
+
+ it('does not pretend to extract embedded JavaScript or dynamic targets', () => {
+ const result = extract('{% capture title %}{% render target %}{% endcapture %}');
+ expect(result.nodes.some(n => n.kind === 'function' || n.name === 'hello')).toBe(false);
+ expect(result.unresolvedReferences).toEqual([]);
+ });
+});
diff --git a/src/extraction/liquid-extractor.ts b/src/extraction/liquid-extractor.ts
index f6d9aee60a..102cf23c10 100644
--- a/src/extraction/liquid-extractor.ts
+++ b/src/extraction/liquid-extractor.ts
@@ -50,7 +50,7 @@ export class LiquidExtractor {
// Extract schema block
this.extractSchema(fileNode.id);
- // Extract assign statements as variables
+ // Extract assign and capture declarations as variables
this.extractAssignments(fileNode.id);
}
} catch (error) {
@@ -411,11 +411,15 @@ export class LiquidExtractor {
}
/**
- * Extract {% assign var = value %} statements
+ * Extract assign and capture variable declarations in either tag spelling.
*/
private extractAssignments(fileNodeId: string): void {
// Both spellings. Most of a modern theme's assigns are the bare kind.
- for (const match of this.findTagOccurrences('assign', `(\\w+)\\s*=`)) {
+ const declarations = [
+ ...this.findTagOccurrences('assign', `(\\w+)\\s*=`),
+ ...this.findTagOccurrences('capture', `([A-Za-z_][\\w-]*)(?=\\s*(?:-?%\\}|$|#))`),
+ ].sort((a, b) => a.index - b.index);
+ for (const match of declarations) {
const variableName = match.groups[1];
const line = this.getLineNumber(match.index);
diff --git a/src/mcp/server-instructions.ts b/src/mcp/server-instructions.ts
index 80a6247a80..07566aa4d9 100644
--- a/src/mcp/server-instructions.ts
+++ b/src/mcp/server-instructions.ts
@@ -74,6 +74,7 @@ calls; a grep/read exploration is dozens.
- If a tool reports a project isn't indexed (no \`.codegraph/\`), stop calling codegraph tools for that project for the rest of the session and use your built-in tools there instead. Indexing is the user's decision — mention they can run \`codegraph init\` if it comes up, but don't run it yourself.
- Index lags file writes by ~1 second.
- Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates.
+- Liquid has partial dialect coverage: assign/capture definitions and literal Shopify file references are indexed, but embedded JS/CSS, Jekyll includes and runtime settings/translations are not; a missing edge there is not proof of unused code.
- No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have.
`;
From 9ffb0e87e75b46fc46c8dbe1843c844075a325f5 Mon Sep 17 00:00:00 2001
From: Puma7 <88577358+Puma7@users.noreply.github.com>
Date: Tue, 13 Oct 2026 00:01:58 +0200
Subject: [PATCH 2/2] Normalize changelog line endings without changing content
---
CHANGELOG.md | 2874 +++++++++++++++++++++++++-------------------------
1 file changed, 1437 insertions(+), 1437 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e60bd6c5d6..70e09e9695 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1437 +1,1437 @@
-# Changelog
-
-All notable changes to CodeGraph are documented here. Each entry also ships as
-a [GitHub Release](https://github.com/colbymchenry/codegraph/releases) tagged
-`vX.Y.Z`, which is where most people will look.
-
-Each release opens with a short **Highlights** list — the handful of things most
-users will notice — followed by the full notes.
-
-This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
-and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## [Unreleased]
-
-### Fixes
-
-- Liquid capture variables, including quoted names, are now searchable in plain templates and multiline Liquid tags, and the documented coverage distinguishes standard Liquid declarations from Shopify dependencies and unsupported host features. (#2498)
-- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
-- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused.
-- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
-- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
-- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337)
-- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
-- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
-- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
-- In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name.
-- VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it.
-- Upgrading CodeGraph while an agent session is open no longer leaves the old version's background server in charge of your project: the first session started from the new install stops it and starts a current one in its place, even while sessions opened before the upgrade are still running. That old server could no longer load the language parsers the upgrade removed, so it saved every file it re-indexed with no symbols, while new sessions could only read the index beside it without keeping it up to date. A background server from a newer install is never stopped, and sessions opened before the upgrade keep the old version until you restart them. Thanks @lipchey for the report. (#2335)
-- A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335)
-- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
-- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
-- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
-- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `