mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(liquid): JSON under templates/ or sections/ is Liquid only inside a Shopify theme (#2393)
isShopifyLiquidJson matched every templates/**/*.json and sections/**/*.json by path alone, so a .NET project template's template.json (jasontaylordev/ CleanArchitecture), schematics and CMS content were indexed as empty Liquid files, and JSON listing `sections` could link to an unrelated Liquid file. The rule now needs the directory holding templates/ or sections/ to also hold layout/theme.liquid or config/settings_schema.json, looked up under the project root. isSourceFile, detectLanguage and preloadLanguagesForFiles take an optional rootDir, and every caller that decides what to index or how to parse passes it: the scans, git change detection, scoped sync, the watcher, indexAll, indexFileWithContent and CodeGraph.extractFromSource. Without a root there is no theme to find. A sync removes JSON an older index stored this way. Shopify/dawn, expressjs/express and gofynd/Astra index byte-identically; CleanArchitecture loses its 2 bogus Liquid files. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, an annotation is no longer lost when a comment sits between it and the declaration, like `@override` followed by `// ignore: must_call_super`, a `///` comment written below the annotations, or `@riverpod` followed by an `// expect_lint: …` line. Before, the comment hid the annotation, so a provider written that way was missing from the annotation's `callers` and impact. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call written through an import prefix, like `http.get(…)` after `import 'package:http/http.dart' as http;`, or `utils.Listener()` and `kit.Box.create()` through a prefix of your own, now links to what the library imported with that prefix exports, following its `export` chains, `show` and `hide`, and to nothing when that library is a package outside your project. Before, the prefix was taken for a variable, so `http.get(…)` could link to an unrelated `Http` class's `get` method, and calls made through a prefix into your own libraries were missing from `callers` and impact. Re-index Dart projects after upgrading.
|
||||
- `codegraph sync` and the file watcher now link an import to the file it names when that file is added after the importing file was indexed, or comes back after being deleted: `import 'package:app/b.dart';` in Dart, `import './side'` in JavaScript or TypeScript, `#include "net/socket.h"` in C and C++, `require 'inc/db.php'` in PHP, `import pkg.mod` in Python, a Lua `require`, or a C# `using` of a namespace declared later. Before, the import stayed unlinked until the importing file itself changed or the project was re-indexed, so `codegraph affected` and the files listed as using the new file missed it. Re-index after upgrading.
|
||||
- JSON files in a `templates` or `sections` folder are now read as Shopify Liquid only inside a Shopify theme, where the folder that contains `templates` or `sections` also has `layout/theme.liquid` or `config/settings_schema.json`. Before, all such JSON was, so a .NET project template's `template.json`, a schematic or a CMS's content showed up in the index as an empty Liquid file, and one that lists `sections` could be linked to an unrelated Liquid file of the same name. Shopify themes, including one kept in a subfolder of a repository, are indexed as before, and the next sync removes JSON that an earlier version indexed this way.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -7427,6 +7427,9 @@ describe('Liquid Shopify JSON template section resolution', () => {
|
||||
// looked unused. The JSON is now indexed and its `type`s linked.
|
||||
fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, 'templates/customers'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, 'layout'), { recursive: true });
|
||||
// Every Shopify theme has layout/theme.liquid beside templates/ and sections/.
|
||||
fs.writeFileSync(path.join(tempDir, 'layout/theme.liquid'), `{{ content_for_layout }}\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'sections/main-login.liquid'), `<form>{{ 'customer.login' | t }}</form>\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'templates/product.json'), JSON.stringify({ sections: { main: { type: 'main-product' } }, order: ['main'] }));
|
||||
@@ -7444,6 +7447,101 @@ describe('Liquid Shopify JSON template section resolution', () => {
|
||||
expect(cg.getFileDependents(product!.filePath).some((p) => p.endsWith('templates/product.json')), 'top-level JSON template links its section').toBe(true);
|
||||
expect(cg.getFileDependents(login!.filePath).some((p) => p.endsWith('customers/login.json')), 'nested JSON template links its section').toBe(true);
|
||||
});
|
||||
|
||||
const jsonTemplate = (type: string): string => JSON.stringify({ sections: { main: { type } }, order: ['main'] });
|
||||
const indexedJson = (): string[] =>
|
||||
cg.getNodesByKind('file').map((n) => n.filePath).filter((p) => p.endsWith('.json')).sort();
|
||||
|
||||
it('leaves JSON under templates/ or sections/ out when no Shopify theme holds it', async () => {
|
||||
// jasontaylordev/CleanArchitecture ships a .NET project template under
|
||||
// templates/, and a CMS keeps its content under sections/: JSON in folders
|
||||
// with those names, but nothing beside them makes a Shopify theme, so it is
|
||||
// not Liquid — even when a Liquid file is named like one of its "types".
|
||||
fs.mkdirSync(path.join(tempDir, 'templates/ca-use-case/.template.config'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'templates/ca-use-case/.template.config/dotnetcli.host.json'), JSON.stringify({ symbolInfo: { UseCase: { longName: 'name' } } }));
|
||||
fs.writeFileSync(path.join(tempDir, 'templates/ca-use-case/.template.config/template.json'), JSON.stringify({ identity: 'CleanArchitecture.UseCase', sections: { main: { type: 'hero' } } }));
|
||||
fs.writeFileSync(path.join(tempDir, 'sections/home.json'), jsonTemplate('hero'));
|
||||
fs.writeFileSync(path.join(tempDir, 'sections/hero.liquid'), `<h1>{{ page.title }}</h1>\n`);
|
||||
|
||||
cg = CodeGraph.initSync(tempDir);
|
||||
await cg.indexAll();
|
||||
cg.resolveReferences();
|
||||
|
||||
expect(indexedJson()).toEqual([]);
|
||||
const hero = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/hero.liquid'));
|
||||
expect(hero, 'the Liquid file itself is still indexed').toBeDefined();
|
||||
expect(cg.getFileDependents(hero!.filePath)).toEqual([]);
|
||||
});
|
||||
|
||||
it('finds a Shopify theme in a subdirectory by its config/settings_schema.json', async () => {
|
||||
// A theme under theme/ with no layout/theme.liquid committed: its settings
|
||||
// schema marks it. The same JSON under tools/templates/ is in no theme.
|
||||
for (const dir of ['theme/config', 'theme/sections', 'theme/templates', 'tools/templates']) {
|
||||
fs.mkdirSync(path.join(tempDir, dir), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(path.join(tempDir, 'theme/config/settings_schema.json'), JSON.stringify([{ name: 'theme_info' }]));
|
||||
fs.writeFileSync(path.join(tempDir, 'theme/sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'theme/templates/product.json'), jsonTemplate('main-product'));
|
||||
fs.writeFileSync(path.join(tempDir, 'tools/templates/product.json'), jsonTemplate('main-product'));
|
||||
|
||||
cg = CodeGraph.initSync(tempDir);
|
||||
await cg.indexAll();
|
||||
cg.resolveReferences();
|
||||
|
||||
expect(indexedJson()).toEqual(['theme/templates/product.json']);
|
||||
expect(cg.getFileDependents('theme/sections/main-product.liquid')).toEqual(['theme/templates/product.json']);
|
||||
// Extracting without storing tells them apart the same way.
|
||||
const refsOf = (file: string) => cg.extractFromSource(file, jsonTemplate('main-product')).unresolvedReferences.map((r) => r.referenceName);
|
||||
expect(refsOf('theme/templates/product.json')).toEqual(['sections/main-product.liquid']);
|
||||
expect(refsOf('tools/templates/product.json')).toEqual([]);
|
||||
});
|
||||
|
||||
it('drops JSON templates once their folder is no longer a theme, and takes them back when it is', async () => {
|
||||
// The same path an index built before this check takes: JSON it stored as
|
||||
// Liquid outside any theme leaves on the next sync, without a re-index.
|
||||
for (const dir of ['layout', 'sections', 'templates']) fs.mkdirSync(path.join(tempDir, dir), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'layout/theme.liquid'), `{{ content_for_layout }}\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
|
||||
fs.writeFileSync(path.join(tempDir, 'templates/page.json'), jsonTemplate('main-product'));
|
||||
fs.writeFileSync(path.join(tempDir, 'templates/product.json'), jsonTemplate('main-product'));
|
||||
cg = CodeGraph.initSync(tempDir);
|
||||
await cg.indexAll();
|
||||
expect(indexedJson()).toEqual(['templates/page.json', 'templates/product.json']);
|
||||
|
||||
fs.rmSync(path.join(tempDir, 'layout'), { recursive: true, force: true });
|
||||
// A sync of only the reported paths (the watcher's) and a full sync agree.
|
||||
await cg.sync({ paths: ['templates/product.json'] });
|
||||
expect(indexedJson()).toEqual(['templates/page.json']);
|
||||
await cg.sync();
|
||||
expect(indexedJson()).toEqual([]);
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, 'config'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'config/settings_schema.json'), '[]');
|
||||
await cg.sync();
|
||||
expect(indexedJson()).toEqual(['templates/page.json', 'templates/product.json']);
|
||||
});
|
||||
|
||||
it('reads JSON under templates/ or sections/ as Liquid only inside a theme under the project root', () => {
|
||||
fs.mkdirSync(path.join(tempDir, 'shop/layout'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'shop/layout/theme.liquid'), '');
|
||||
|
||||
for (const file of ['shop/templates/product.json', 'shop/templates/customers/login.json', 'shop/sections/header-group.json']) {
|
||||
expect(isSourceFile(file, undefined, tempDir), file).toBe(true);
|
||||
expect(detectLanguage(file, undefined, undefined, tempDir), file).toBe('liquid');
|
||||
// Without the project root there is no theme to look for.
|
||||
expect(isSourceFile(file), file).toBe(false);
|
||||
expect(detectLanguage(file), file).toBe('unknown');
|
||||
}
|
||||
// Beside the theme, not in it; and the theme's other JSON was never a template.
|
||||
for (const file of ['templates/product.json', 'other/sections/header-group.json', 'shop/config/settings_data.json']) {
|
||||
expect(isSourceFile(file, undefined, tempDir), file).toBe(false);
|
||||
expect(detectLanguage(file, undefined, undefined, tempDir), file).toBe('unknown');
|
||||
}
|
||||
// Outside a theme, JSON gets whatever the project maps `.json` to, as any other JSON does.
|
||||
expect(detectLanguage('templates/product.json', undefined, { '.json': 'yaml' }, tempDir)).toBe('yaml');
|
||||
expect(detectLanguage('shop/templates/product.json', undefined, { '.json': 'yaml' }, tempDir)).toBe('liquid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lua/Luau require resolution', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsp from 'fs/promises';
|
||||
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
|
||||
import { ExtractionError, Language } from '../types';
|
||||
@@ -241,10 +242,14 @@ export function hasMpegTsExtension(filePath: string): boolean {
|
||||
* `overrides` is the project's validated custom extension → language map (from
|
||||
* `codegraph.json`); when present its extensions count as indexable in addition
|
||||
* to the built-ins. Omitting it is byte-identical to the zero-config behavior.
|
||||
*
|
||||
* `rootDir` is the project root `filePath` is relative to. A Shopify theme's
|
||||
* JSON templates count only when it is given, since telling a theme apart takes
|
||||
* a look at the files beside them (see `isShopifyLiquidJson`).
|
||||
*/
|
||||
export function isSourceFile(filePath: string, overrides?: Record<string, Language>): boolean {
|
||||
export function isSourceFile(filePath: string, overrides?: Record<string, Language>, rootDir?: string): boolean {
|
||||
if (isPlayRoutesFile(filePath)) return true; // Play `conf/routes` is extensionless
|
||||
if (isShopifyLiquidJson(filePath)) return true; // Shopify OS 2.0 JSON templates / section groups
|
||||
if (isShopifyLiquidJson(filePath, rootDir)) return true; // Shopify OS 2.0 JSON templates / section groups
|
||||
if (isErlangAppFile(filePath)) return true; // OTP `.app`/`.app.src` resource files
|
||||
const dot = filePath.lastIndexOf('.');
|
||||
if (dot < 0) return false;
|
||||
@@ -252,15 +257,35 @@ export function isSourceFile(filePath: string, overrides?: Record<string, Langua
|
||||
return ext in EXTENSION_MAP || (!!overrides && ext in overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* What marks a Shopify theme's root, beside its `templates/` and `sections/`:
|
||||
* the `layout/theme.liquid` every theme must have, or its settings schema.
|
||||
*/
|
||||
const SHOPIFY_THEME_MARKERS = ['layout/theme.liquid', 'config/settings_schema.json'];
|
||||
|
||||
/**
|
||||
* Shopify OS 2.0 JSON template (`templates/*.json`) or section group
|
||||
* (`sections/*.json`) — these reference sections by `"type"`, so the Liquid
|
||||
* extractor links them. (config/ + locales/ JSON have no section refs.)
|
||||
*
|
||||
* Only inside a Shopify theme: the directory holding that `templates/` or
|
||||
* `sections/` must also hold one of `SHOPIFY_THEME_MARKERS`. Plenty of other
|
||||
* projects keep JSON in folders with those names — a .NET project template's
|
||||
* `templates/<name>/.template.config/template.json`, schematics, CMS content —
|
||||
* and it is not Liquid. The theme is looked for under `rootDir`, the project
|
||||
* root `filePath` is relative to; without one there is no theme to find.
|
||||
*/
|
||||
export function isShopifyLiquidJson(filePath: string): boolean {
|
||||
export function isShopifyLiquidJson(filePath: string, rootDir?: string): boolean {
|
||||
// Allow nested template dirs (`templates/customers/login.json`), not just
|
||||
// top-level (`templates/product.json`).
|
||||
return /(^|\/)(templates|sections)\/.+\.json$/i.test(filePath);
|
||||
if (rootDir === undefined || !/(^|\/)(templates|sections)\/.+\.json$/i.test(filePath)) return false;
|
||||
const segments = filePath.split('/');
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
if (!/^(templates|sections)$/i.test(segments[i]!)) continue;
|
||||
const themeDir = path.join(rootDir, ...segments.slice(0, i));
|
||||
if (SHOPIFY_THEME_MARKERS.some((marker) => fs.existsSync(path.join(themeDir, marker)))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -536,15 +561,18 @@ export function getParser(language: Language): Parser | null {
|
||||
* `overrides` is the project's validated custom extension → language map (from
|
||||
* `codegraph.json`); when present its mappings take precedence over the built-in
|
||||
* `EXTENSION_MAP`. Omitting it is byte-identical to the zero-config behavior.
|
||||
*
|
||||
* `rootDir` is the project root `filePath` is relative to, as for
|
||||
* `isSourceFile`: a Shopify theme's JSON templates are Liquid only with it.
|
||||
*/
|
||||
export function detectLanguage(filePath: string, source?: string, overrides?: Record<string, Language>): Language {
|
||||
export function detectLanguage(filePath: string, source?: string, overrides?: Record<string, Language>, rootDir?: string): Language {
|
||||
// Play `conf/routes` has no grammar — route through the no-symbol path; the
|
||||
// Play framework resolver extracts route nodes from it.
|
||||
if (isPlayRoutesFile(filePath)) return 'yaml';
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
||||
// Shopify OS 2.0 JSON templates / section groups → the Liquid extractor (it
|
||||
// links each section `"type"` to its `sections/<type>.liquid`).
|
||||
if (isShopifyLiquidJson(filePath)) return 'liquid';
|
||||
// A Shopify theme's OS 2.0 JSON templates / section groups → the Liquid
|
||||
// extractor (it links each section `"type"` to its `sections/<type>.liquid`).
|
||||
if (isShopifyLiquidJson(filePath, rootDir)) return 'liquid';
|
||||
// OTP `.app`/`.app.src` resource files — Erlang terms the grammar parses as
|
||||
// top-level expressions (last-dot ext `.src` is too generic for the map).
|
||||
if (isErlangAppFile(filePath)) return 'erlang';
|
||||
|
||||
+24
-22
@@ -602,7 +602,7 @@ function collectIncludedFiles(
|
||||
if (defaults.ignores(rel)) return;
|
||||
if (!include.ignores(rel)) return;
|
||||
if (exclude && exclude.ignores(rel)) return;
|
||||
if (!isSourceFile(rel, overrides)) return;
|
||||
if (!isSourceFile(rel, overrides, rootDir)) return;
|
||||
out.add(rel);
|
||||
}
|
||||
};
|
||||
@@ -811,9 +811,10 @@ function findNestedGitRepos(absDir: string, relPrefix: string): string[] {
|
||||
*/
|
||||
export function preloadLanguagesForFiles(
|
||||
files: string[],
|
||||
overrides?: Record<string, Language>
|
||||
overrides?: Record<string, Language>,
|
||||
rootDir?: string
|
||||
): Language[] {
|
||||
const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides)))];
|
||||
const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides, rootDir)))];
|
||||
// A Flow-typed `.js` is read with the TSX grammar (see detectLanguage).
|
||||
if ((languages.includes('javascript') || languages.includes('jsx')) && !languages.includes('tsx')) languages.push('tsx');
|
||||
if (languages.includes('c')) {
|
||||
@@ -1358,7 +1359,8 @@ export function getGitChangedFiles(rootDir: string, sinceCommit?: string | null)
|
||||
// Custom extension → language overrides from the project's codegraph.json,
|
||||
// so change detection sees the same custom-extension files the full index does.
|
||||
const overrides = loadExtensionOverrides(rootDir);
|
||||
collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined);
|
||||
const isSource = (filePath: string): boolean => isSourceFile(filePath, overrides, rootDir);
|
||||
collectGitStatus(rootDir, '', changes, isSource, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined);
|
||||
return changes;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -1430,7 +1432,7 @@ export function canTrustGitFastPath(rootDir: string, sinceCommit?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null, sinceCommit?: string): void {
|
||||
function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, isSource: (filePath: string) => boolean, includeIgnored: Ignore | null = null, exclude: Ignore | null = null, sinceCommit?: string): void {
|
||||
const output = execFileSync(
|
||||
'git',
|
||||
// `-uall` lists individual untracked files instead of collapsing an
|
||||
@@ -1459,7 +1461,7 @@ function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, over
|
||||
// filtered by exactly the rules a working-tree change is (#766, #999, #1829).
|
||||
const classify = (statusCode: string, rel: string): void => {
|
||||
const filePath = normalizePath(prefix + rel);
|
||||
if (!isSourceFile(filePath, overrides)) return;
|
||||
if (!isSource(filePath)) return;
|
||||
|
||||
if (statusCode.includes('D')) {
|
||||
// Deletions stay unfiltered: getChangedFiles acts on one only when the
|
||||
@@ -1523,11 +1525,11 @@ function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, over
|
||||
// and they are left alone (#970, #976), mirroring the full-index scan.
|
||||
for (const rel of untrackedDirs) {
|
||||
for (const repoRel of findNestedGitRepos(path.join(repoDir, rel), rel)) {
|
||||
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored, exclude);
|
||||
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, isSource, includeIgnored, exclude);
|
||||
}
|
||||
}
|
||||
for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
|
||||
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored, exclude);
|
||||
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, isSource, includeIgnored, exclude);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1551,7 +1553,7 @@ export function scanDirectory(
|
||||
const files: string[] = [];
|
||||
let count = 0;
|
||||
for (const filePath of gitFiles) {
|
||||
if (isSourceFile(filePath, overrides)) {
|
||||
if (isSourceFile(filePath, overrides, rootDir)) {
|
||||
files.push(filePath);
|
||||
count++;
|
||||
onProgress?.(count, filePath);
|
||||
@@ -1602,7 +1604,7 @@ export async function scanDirectoryAsync(
|
||||
const files: string[] = [];
|
||||
let count = 0;
|
||||
for (const filePath of gitFiles) {
|
||||
if (isSourceFile(filePath, overrides)) {
|
||||
if (isSourceFile(filePath, overrides, rootDir)) {
|
||||
files.push(filePath);
|
||||
count++;
|
||||
onProgress?.(count, filePath);
|
||||
@@ -1710,7 +1712,7 @@ function scanDirectoryWalk(
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (!isIgnored(fullPath, false, active)) {
|
||||
if (isSourceFile(relativePath, overrides)) {
|
||||
if (isSourceFile(relativePath, overrides, rootDir)) {
|
||||
files.push(relativePath);
|
||||
count++;
|
||||
onProgress?.(count, relativePath);
|
||||
@@ -1731,7 +1733,7 @@ function scanDirectoryWalk(
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
if (!isIgnored(fullPath, false, active)) {
|
||||
if (isSourceFile(relativePath, overrides)) {
|
||||
if (isSourceFile(relativePath, overrides, rootDir)) {
|
||||
files.push(relativePath);
|
||||
count++;
|
||||
onProgress?.(count, relativePath);
|
||||
@@ -2186,7 +2188,7 @@ export class ExtractionOrchestrator {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
// Detect needed languages and load grammars in the parse worker
|
||||
const neededLanguages = preloadLanguagesForFiles(files, overrides);
|
||||
const neededLanguages = preloadLanguagesForFiles(files, overrides, this.rootDir);
|
||||
|
||||
// Parse files on a pool of worker threads (keeps the main thread free for UI
|
||||
// and uses every core). Falls back to in-process parsing when the compiled
|
||||
@@ -2257,7 +2259,7 @@ export class ExtractionOrchestrator {
|
||||
* here on the main thread, where the codegraph.json overrides are loaded.
|
||||
*/
|
||||
const parseFile = (filePath: string, content: string): Promise<ExtractionResult> => {
|
||||
const language = detectLanguage(filePath, content, overrides);
|
||||
const language = detectLanguage(filePath, content, overrides, this.rootDir);
|
||||
const names = this.frameworksForFile(filePath, frameworkNames);
|
||||
if (!pool) return Promise.resolve(extractFromSource(filePath, content, language, names));
|
||||
return pool.requestParse({ filePath, content, language, frameworkNames: names });
|
||||
@@ -2308,7 +2310,7 @@ export class ExtractionOrchestrator {
|
||||
// Store: on the writer thread when active (fresh DB — bundles applied
|
||||
// in the same file order this chain dispatches them), else on the main
|
||||
// thread (SQLite connections are per-thread).
|
||||
const language = detectLanguage(filePath, content, overrides);
|
||||
const language = detectLanguage(filePath, content, overrides, this.rootDir);
|
||||
if (grammarUnavailable) {
|
||||
// Store nothing: a row from an earlier run keeps its data, and with no
|
||||
// row (or an older hash) the next sync or index retries the file.
|
||||
@@ -2352,7 +2354,7 @@ export class ExtractionOrchestrator {
|
||||
// Files with no symbols but no errors (yaml, twig, properties) are
|
||||
// tracked at the file level — count them as indexed so the CLI doesn't
|
||||
// misleadingly report "No files found to index".
|
||||
const lang = detectLanguage(filePath, content, overrides);
|
||||
const lang = detectLanguage(filePath, content, overrides, this.rootDir);
|
||||
if (isFileLevelOnlyLanguage(lang)) {
|
||||
filesIndexed++;
|
||||
} else {
|
||||
@@ -2640,7 +2642,7 @@ export class ExtractionOrchestrator {
|
||||
// so decode here — otherwise a kernel-language retry passes the gate
|
||||
// below via `errors.length === 0`, stores nothing, and the file is
|
||||
// permanently recorded as "(0 symbols)" with the error erased (#1541).
|
||||
const language = detectLanguage(filePath, content, overrides);
|
||||
const language = detectLanguage(filePath, content, overrides, this.rootDir);
|
||||
result = materializeKernelResult(result, filePath, language);
|
||||
|
||||
if (result.nodes.length > 0 || result.errors.length === 0) {
|
||||
@@ -2695,7 +2697,7 @@ export class ExtractionOrchestrator {
|
||||
}
|
||||
|
||||
// Same undecoded-transport hazard as the first retry pass (#1541).
|
||||
const language = detectLanguage(filePath, fullContent, overrides);
|
||||
const language = detectLanguage(filePath, fullContent, overrides, this.rootDir);
|
||||
result = materializeKernelResult(result, filePath, language);
|
||||
|
||||
if (result.nodes.length > 0 || result.errors.length === 0) {
|
||||
@@ -2857,7 +2859,7 @@ export class ExtractionOrchestrator {
|
||||
};
|
||||
}
|
||||
|
||||
const language = detectLanguage(relativePath, content, loadExtensionOverrides(this.rootDir));
|
||||
const language = detectLanguage(relativePath, content, loadExtensionOverrides(this.rootDir), this.rootDir);
|
||||
|
||||
// Check file size
|
||||
if (stats.size > MAX_SOURCE_FILE_SIZE_BYTES) {
|
||||
@@ -3350,7 +3352,7 @@ export class ExtractionOrchestrator {
|
||||
const overrides = loadExtensionOverrides(this.rootDir);
|
||||
currentFiles = unique.filter(
|
||||
(p) =>
|
||||
isSourceFile(p, overrides) &&
|
||||
isSourceFile(p, overrides, this.rootDir) &&
|
||||
!scope.ignores(p) &&
|
||||
fs.existsSync(path.join(this.rootDir, p))
|
||||
);
|
||||
@@ -3500,7 +3502,7 @@ export class ExtractionOrchestrator {
|
||||
// Load only grammars needed for changed files
|
||||
if (filesToIndex.length > 0) {
|
||||
const overrides = loadExtensionOverrides(this.rootDir);
|
||||
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides));
|
||||
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides, this.rootDir));
|
||||
}
|
||||
|
||||
// Index changed files
|
||||
@@ -3627,7 +3629,7 @@ export class ExtractionOrchestrator {
|
||||
for (const filePath of candidates) {
|
||||
const tracked = this.queries.getFileByPath(filePath);
|
||||
const fullPath = path.join(this.rootDir, filePath);
|
||||
if (!isSourceFile(filePath, overrides) || scope.ignores(filePath) || !fs.existsSync(fullPath)) {
|
||||
if (!isSourceFile(filePath, overrides, this.rootDir) || scope.ignores(filePath) || !fs.existsSync(fullPath)) {
|
||||
if (tracked) removed.push(filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
+4
-2
@@ -45,7 +45,7 @@ import {
|
||||
extractFromSource,
|
||||
initGrammars,
|
||||
} from './extraction';
|
||||
import { hasGrammarLoadFailure, isFileLevelOnlyLanguage } from './extraction/grammars';
|
||||
import { detectLanguage, hasGrammarLoadFailure, isFileLevelOnlyLanguage } from './extraction/grammars';
|
||||
import {
|
||||
ReferenceResolver,
|
||||
createResolver,
|
||||
@@ -1415,7 +1415,9 @@ export class CodeGraph {
|
||||
* Extract nodes and edges from source code (without storing)
|
||||
*/
|
||||
extractFromSource(filePath: string, source: string): ExtractionResult {
|
||||
return extractFromSource(filePath, source);
|
||||
// The project root is what tells a Shopify theme's JSON templates (Liquid)
|
||||
// apart from other JSON.
|
||||
return extractFromSource(filePath, source, detectLanguage(filePath, source, undefined, this.projectRoot));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
+1
-1
@@ -650,7 +650,7 @@ export class FileWatcher {
|
||||
this.refreshScope(rel);
|
||||
return;
|
||||
}
|
||||
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) {
|
||||
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot), this.projectRoot)) {
|
||||
this.maybeScheduleForRemovedDir(rel);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user