mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
* fix(sync): a Liquid section or snippet reference links its file when the file appears later
A sync retries a parked failed reference by its name tail, matched against
the names of the nodes the changed files carry; a file's node is named for
the file. Liquid records `{% render 'price' %}`, `{% include %}`,
`{% section %}` and a JSON template's section `type` as references named
`snippets/price.liquid` / `sections/404.liquid`, and a failed one was parked
under the text after its last dot: `liquid`, the extension, which no node is
named. So a section or snippet added after the files naming it (or restored
after a delete) stayed unlinked until a referencing file changed or the
project was re-indexed, even in a repository with a single theme.
A `references` ref whose name is a path ending in a file name with an
extension is now parked under that file name (`price.liquid`), the key path
matching looks the file up by. The existing symbol-name retry then finds it
with no new query, since the changed files' node names already include each
file's own. Calls are left alone: their slashes sit in string arguments,
comments or divisions. Schema v13 rewrites the tails an older version
parked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(liquid): compare the synced links with a fresh index of the same files
Indexing again over an existing index skips files whose content is
unchanged, so it kept the synced links of the files that name the new ones
instead of resolving them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
6.8 KiB
TypeScript
139 lines
6.8 KiB
TypeScript
/**
|
|
* A sync links a reference written as a path once the file it names appears.
|
|
*
|
|
* Sync retries parked failed refs by name tail (#1240): the tail is matched
|
|
* against the names of the nodes a sync adds, and a file's node is named for
|
|
* the file. A reference written as a path — Liquid's `{% render 'price' %}` is
|
|
* `snippets/price.liquid`, a JSON template's section `"type": "404"` is
|
|
* `sections/404.liquid` — was parked under its extension, `liquid`, which no
|
|
* node is named, so it linked only once the file naming it changed or on a
|
|
* full index. #2392 fixed the same gap for imports.
|
|
*/
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
import { DatabaseConnection, getDatabasePath } from '../src/db';
|
|
import { CURRENT_SCHEMA_VERSION, getCurrentVersion, runMigrations } from '../src/db/migrations';
|
|
import { QueryBuilder } from '../src/db/queries';
|
|
import { referenceNameTail } from '../src/db/reference-tail';
|
|
|
|
let root: string | undefined;
|
|
let cg: CodeGraph | undefined;
|
|
let db: DatabaseConnection | undefined;
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = undefined;
|
|
db?.close();
|
|
db = undefined;
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
root = undefined;
|
|
});
|
|
|
|
function write(files: Record<string, string>): void {
|
|
for (const [file, text] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(root!, file)), { recursive: true });
|
|
fs.writeFileSync(path.join(root!, file), text);
|
|
}
|
|
}
|
|
|
|
describe('referenceNameTail', () => {
|
|
it('parks a reference written as a path under the name of the file it names', () => {
|
|
expect(referenceNameTail('snippets/price.liquid', 'references')).toBe('price.liquid');
|
|
expect(referenceNameTail('sections/404.liquid', 'references')).toBe('404.liquid');
|
|
expect(referenceNameTail('snippets/icon.logo.liquid', 'references')).toBe('icon.logo.liquid');
|
|
expect(referenceNameTail('lazy-import:./routes/about.tsx', 'references')).toBe('about.tsx');
|
|
});
|
|
|
|
it('keeps the symbol tail of a reference that names no file', () => {
|
|
// No path, a path with no file name at its end, or an Erlang arity.
|
|
expect(referenceNameTail('Foo.Bar', 'references')).toBe('Bar');
|
|
expect(referenceNameTail('import:./home.component#HomeComponent', 'references')).toBe('component#HomeComponent');
|
|
expect(referenceNameTail('lists::map/2', 'references')).toBe('map');
|
|
// A call's slashes sit in its arguments, a comment or a division.
|
|
expect(referenceNameTail('assert.logfile("logs/error.log").has.line', 'calls')).toBe('line');
|
|
expect(referenceNameTail('(rss_kb / 1024.0).to_i', 'calls')).toBe('to_i');
|
|
// An import keeps the stem of its path (#2392).
|
|
expect(referenceNameTail('package:app/b.dart', 'imports')).toBe('b');
|
|
});
|
|
});
|
|
|
|
describe('sync links a path reference whose file appears later', () => {
|
|
it('in an index whose references an older version parked', async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-path-ref-'));
|
|
write({
|
|
'layout/theme.liquid': "{% render 'price' %}{{ content_for_layout }}\n",
|
|
'templates/404.json': JSON.stringify({ sections: { main: { type: '404' } }, order: ['main'] }),
|
|
});
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
cg.close();
|
|
cg = undefined;
|
|
|
|
// What a version before 13 left: the extension as the tail.
|
|
db = DatabaseConnection.open(getDatabasePath(root));
|
|
db.getDb().exec(`UPDATE unresolved_refs SET name_tail = 'liquid' WHERE reference_kind = 'references';
|
|
DELETE FROM schema_versions WHERE version >= 13;
|
|
INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (12, 0, 'legacy fixture');`);
|
|
db.close();
|
|
db = undefined;
|
|
|
|
cg = await CodeGraph.open(root);
|
|
write({ 'sections/404.liquid': '<h1>404</h1>\n', 'snippets/price.liquid': '{{ product.price }}\n' });
|
|
expect((await cg.sync()).filesAdded).toBe(2);
|
|
expect(cg.getFileDependencies('templates/404.json')).toEqual(['sections/404.liquid']);
|
|
expect(cg.getFileDependencies('layout/theme.liquid')).toEqual(['snippets/price.liquid']);
|
|
expect(cg.getPendingReferenceCount()).toBe(0);
|
|
}, 60_000);
|
|
});
|
|
|
|
describe('schema v13', () => {
|
|
function fixture(): QueryBuilder {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-path-ref-'));
|
|
db = DatabaseConnection.initialize(path.join(root, 'test.db'));
|
|
const queries = new QueryBuilder(db.getDb());
|
|
queries.insertNode({ id: 'a', kind: 'file', name: 'theme.liquid', qualifiedName: 'layout/theme.liquid', filePath: 'layout/theme.liquid',
|
|
language: 'liquid', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0 });
|
|
for (const [referenceName, referenceKind, status, tail] of [
|
|
['snippets/price.liquid', 'references', 'failed', 'liquid'],
|
|
['sections/icon.logo.liquid', 'references', 'failed', 'liquid'],
|
|
['snippets/card.liquid', 'references', 'pending', ''],
|
|
['package:app/b.dart', 'imports', 'failed', 'b'],
|
|
['assert.logfile("logs/error.log").has.line', 'calls', 'failed', 'line'],
|
|
['lists::map/2', 'calls', 'failed', 'map'],
|
|
] as const) {
|
|
queries.insertUnresolvedRef({ fromNodeId: 'a', referenceName, referenceKind, line: 1, column: 0, filePath: 'layout/theme.liquid', language: 'liquid' });
|
|
db.getDb().prepare('UPDATE unresolved_refs SET status = ?, name_tail = ? WHERE reference_name = ?').run(status, tail, referenceName);
|
|
}
|
|
return queries;
|
|
}
|
|
|
|
const tails = () => db!.getDb().prepare('SELECT reference_name AS name, name_tail AS tail FROM unresolved_refs ORDER BY id').all();
|
|
|
|
it('rewrites the tail of a path reference parked by an older version, and replays cleanly', () => {
|
|
fixture();
|
|
db!.getDb().exec(`DELETE FROM schema_versions WHERE version >= 13;
|
|
INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (12, 0, 'legacy fixture');`);
|
|
db!.close();
|
|
db = DatabaseConnection.open(path.join(root!, 'test.db'));
|
|
expect(getCurrentVersion(db.getDb())).toBe(CURRENT_SCHEMA_VERSION);
|
|
const migrated = tails();
|
|
expect(migrated).toEqual([
|
|
{ name: 'snippets/price.liquid', tail: 'price.liquid' },
|
|
{ name: 'sections/icon.logo.liquid', tail: 'icon.logo.liquid' },
|
|
// Only a failed row is looked up by its tail.
|
|
{ name: 'snippets/card.liquid', tail: '' },
|
|
{ name: 'package:app/b.dart', tail: 'b' },
|
|
{ name: 'assert.logfile("logs/error.log").has.line', tail: 'line' },
|
|
{ name: 'lists::map/2', tail: 'map' },
|
|
]);
|
|
const queries = new QueryBuilder(db.getDb());
|
|
expect(queries.getRetryableFailedReferences(['price.liquid', 'liquid']).map((ref) => ref.referenceName)).toEqual(['snippets/price.liquid']);
|
|
|
|
db.getDb().exec('DELETE FROM schema_versions WHERE version >= 13');
|
|
runMigrations(db.getDb(), 12);
|
|
expect(tails()).toEqual(migrated);
|
|
});
|
|
});
|