Skip to content

Commit dc4c1a1

Browse files
Merge pull request #1565 from AxiomCodeAI/fix/refresh-after-edit
Refresh after edit: one engine lookup, no silent fallback, safe swaps, .gitignore honoured
2 parents 18a6cd4 + a85d207 commit dc4c1a1

25 files changed

Lines changed: 654 additions & 97 deletions

‎parser/src/extract.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { JavaScriptProjectAnalyzer } from '@/workflows/javascript/javascript-pro
1515
import { TypeScriptProjectAnalyzer } from '@/workflows/typescript/typescript-project-analyzer';
1616
import { XmlProjectAnalyzer } from '@/workflows/xml/xml-project-analyzer';
1717
import { YamlProjectAnalyzer } from '@/workflows/yaml/yaml-project-analyzer';
18+
import { clearGitIgnored, loadGitIgnored } from '@/utils/git-ignored';
1819

1920
export interface ExtractOptions {
2021
/** Path to the project/codebase to scan. */
@@ -228,6 +229,15 @@ export async function extractProject(opts: ExtractOptions): Promise<void> {
228229
const absolutePath = path.resolve(opts.projectPath);
229230
const scanner = new ProjectScanner();
230231

232+
// The directories git ignores are not the project (utils/git-ignored.ts). A library tree keeps them: a
233+
// dependency's .gitignore names its build output, and for a dependency that output is the source.
234+
if (opts.library === true) {
235+
clearGitIgnored();
236+
} else {
237+
const skipped = loadGitIgnored(absolutePath);
238+
if (skipped > 0) console.log(`🙈 ${skipped} director${skipped === 1 ? 'y' : 'ies'} ignored by git will not be read (AXIOMCODE_NO_GITIGNORE=1 reads them)\n`);
239+
}
240+
231241
console.log('⏳ Scanning for projects...');
232242
// Discovery itself honours the flag: a test directory is never a root (#613).
233243
const allProjects = await scanner.scanForProjects(absolutePath, Infinity, excludeTests);

‎parser/src/utils/git-ignored.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* The directories git ignores under the tree being analysed, so that no walker reads them.
3+
*
4+
* Every walker in the parser skips a fixed list of names (node_modules, dist, .next, …). A generated tree
5+
* that only the repository's .gitignore names — a framework's build cache under another name, a vendored
6+
* bundle, a local export — was parsed as source: an index of a 431-file Next.js/TypeScript project read
7+
* 15,108 files and took 12 minutes. git already knows which directories are not the project, so it is asked
8+
* once per run:
9+
*
10+
* git ls-files --others --ignored --exclude-standard --directory
11+
*
12+
* lists each ignored directory once (not its contents) and only when NOTHING under it is tracked, so a file
13+
* committed on purpose beneath an ignored name is still read, and so is everything outside a git work tree
14+
* (the command fails there, and nothing is skipped). Only directories are skipped, never single ignored
15+
* files: an ignored declaration file (`next-env.d.ts`) still contributes types. The background refresher
16+
* (plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py, git_ignored_dirs) asks the same question, so the
17+
* files it watches are the files the parser reads.
18+
*
19+
* Off for a library tree (`--library`: a dependency's own .gitignore names its build output, which for a
20+
* dependency is the source) and with AXIOMCODE_NO_GITIGNORE=1.
21+
*/
22+
import { execFileSync } from 'child_process';
23+
import * as path from 'path';
24+
25+
let ignored: ReadonlySet<string> = new Set();
26+
27+
const key = (p: string): string => {
28+
const r = path.resolve(p);
29+
return process.platform === 'win32' ? r.toLowerCase() : r;
30+
};
31+
32+
/** Load the ignored directories under `root`; returns how many there are. Replaces what an earlier call loaded. */
33+
export function loadGitIgnored(root: string): number {
34+
ignored = new Set();
35+
if (process.env.AXIOMCODE_NO_GITIGNORE) return 0;
36+
let out: string;
37+
try {
38+
out = execFileSync('git', ['-C', root, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z', '--', '.'],
39+
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 512 * 1024 * 1024, timeout: 120_000 });
40+
} catch {
41+
return 0; // not a work tree, or no git: nothing is skipped
42+
}
43+
const set = new Set<string>();
44+
for (const entry of out.split('\0')) {
45+
if (entry.endsWith('/')) set.add(key(path.join(root, entry)));
46+
}
47+
ignored = set;
48+
return set.size;
49+
}
50+
51+
/** Forget the loaded set (a library tree is parsed with nothing skipped). */
52+
export function clearGitIgnored(): void {
53+
ignored = new Set();
54+
}
55+
56+
/** True when git ignores the directory at `dir` (absolute, or relative to the working directory). */
57+
export function isGitIgnoredDir(dir: string): boolean {
58+
return ignored.size > 0 && ignored.has(key(dir));
59+
}

‎parser/src/utils/project-scanner.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from 'path';
44
import { EXCLUDED_DIRS, isJavaTestDir } from '@/constants/consts';
55
import { ProjectInfo, ProjectLanguage } from '@/types/ProjectInfo';
66
import { ProjectDetector } from '@/utils/project-detector';
7+
import { isGitIgnoredDir } from '@/utils/git-ignored';
78

89
export class ProjectScanner {
910
private detector: ProjectDetector;
@@ -90,6 +91,9 @@ export class ProjectScanner {
9091
}
9192

9293
const subPath = path.join(dirPath, entry.name);
94+
if (isGitIgnoredDir(subPath)) {
95+
continue;
96+
}
9397
await this.scanDirectory(subPath, projects, currentDepth + 1, maxDepth, claimedBelow, excludeTests);
9498
}
9599
} catch (error) {

‎parser/src/workflows/csharp/csharp-project-analyzer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { implicitFrameworkSymbols } from '@/parsers/csharp/extractors/preproc-co
3434
import { EntityUtils } from '@/utils/entity-utils';
3535
import { CsRelationWriter } from '@/workflows/csharp/cs-relation-writer';
3636
import { governingProject, readProjectConfig } from '@/workflows/csharp/cs-project-config';
37+
import { isGitIgnoredDir } from '@/utils/git-ignored';
3738

3839
/**
3940
* Runs the C# front end over one root and writes the fact base.
@@ -488,7 +489,7 @@ async function discoverCsFiles(
488489
for (const entry of entries) {
489490
const full = path.join(current, entry.name);
490491
if (entry.isDirectory()) {
491-
if (!excludes.has(entry.name)) {
492+
if (!excludes.has(entry.name) && !isGitIgnoredDir(full)) {
492493
stack.push(full);
493494
}
494495
continue;

‎parser/src/workflows/gradle/gradle-project-analyzer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { GradleResolutionLinker } from '@/parsers/gradle/gradle-resolution-linke
2828
import { GradleScriptClassifier } from '@/parsers/gradle/gradle-script-classifier';
2929
import { ProjectInfo } from '@/types/ProjectInfo';
3030
import { EntityUtils } from '@/utils/entity-utils';
31+
import { isGitIgnoredDir } from '@/utils/git-ignored';
3132

3233
interface SkippedGradleFile {
3334
filePath: string;
@@ -356,7 +357,7 @@ export class GradleProjectAnalyzer {
356357
// scripts that describe nothing about the project. It is excluded by
357358
// the leading-dot rule; `buildSrc` deliberately is NOT, because its
358359
// build script is a real fact about how the build is assembled.
359-
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
360+
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.') && !isGitIgnoredDir(path.join(dirPath, entry.name))) {
360361
await this.scanForGradleFiles(path.join(dirPath, entry.name), files);
361362
}
362363
} else if (entry.isFile() && GradleScriptClassifier.isGradleFile(entry.name)) {

‎parser/src/workflows/java/java-project-analyzer.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { CodeExtractor } from '@/parsers/code-extractor';
2525
import { TypeRegistryExtractor, ImportExtractor } from '@/parsers/java/extractors';
2626
import { ProjectInfo, ProjectLanguage } from '@/types/ProjectInfo';
2727
import { EntityUtils } from '@/utils/entity-utils';
28+
import { isGitIgnoredDir } from '@/utils/git-ignored';
2829

2930
export class JavaProjectAnalyzer {
3031
private codeExtractor: CodeExtractor;
@@ -320,7 +321,8 @@ export class JavaProjectAnalyzer {
320321
for (const entry of entries) {
321322
if (entry.isDirectory()) {
322323
const isTestDir = isJavaTestDir(entry.name);
323-
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.') && !(excludeTests && isTestDir)) {
324+
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.') && !(excludeTests && isTestDir)
325+
&& !isGitIgnoredDir(path.join(dirPath, entry.name))) {
324326
const subPath = path.join(dirPath, entry.name);
325327
await this.scanForJavaFiles(subPath, files, excludeTests);
326328
}

‎parser/src/workflows/javascript/javascript-project-analyzer.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { JsTypeHeritageRegistry } from '@/analysis-types/javascript/JsTypeHerita
4646
import { JsTypeReferenceRegistry } from '@/analysis-types/javascript/JsTypeReferenceRegistry';
4747
import { JsTypeRegistry } from '@/analysis-types/javascript/JsTypeRegistry';
4848
import { JsVariableRegistry } from '@/analysis-types/javascript/JsVariableRegistry';
49+
import { isGitIgnoredDir } from '@/utils/git-ignored';
4950

5051
/**
5152
* Each relation's header, from its registry, so an EMPTY relation still writes
@@ -733,6 +734,9 @@ function collectJavaScriptFiles(
733734
for (const entry of entries) {
734735
const full = path.join(directory, entry.name);
735736
if (entry.isDirectory()) {
737+
if (isGitIgnoredDir(full) && !(directory === rootDir && walkUnderRoot.has(entry.name))) {
738+
continue;
739+
}
736740
if (!excludes.has(entry.name) || (directory === rootDir && walkUnderRoot.has(entry.name))) {
737741
walk(full);
738742
continue;

‎parser/src/workflows/properties/properties-project-analyzer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PropertiesParser } from '@/parsers/properties/properties-parser';
1010
import { ProjectInfo } from '@/types/ProjectInfo';
1111
import { EntityUtils } from '@/utils/entity-utils';
1212
import { groupOwnedFiles, resolveFileOwners } from '@/utils/file-ownership';
13+
import { isGitIgnoredDir } from '@/utils/git-ignored';
1314

1415
/**
1516
* Analyzes .properties files within Java projects and extracts
@@ -194,7 +195,7 @@ export class PropertiesProjectAnalyzer {
194195

195196
for (const entry of entries) {
196197
if (entry.isDirectory()) {
197-
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
198+
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.') && !isGitIgnoredDir(path.join(dirPath, entry.name))) {
198199
const subPath = path.join(dirPath, entry.name);
199200
await this.scanForPropertiesFiles(subPath, files);
200201
}

‎parser/src/workflows/python/python-project-analyzer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
PythonResolutionLinker,
2121
} from '@/parsers/python/extractors/python-resolution-linker';
2222
import { Python2Finding } from '@/parsers/python/types';
23+
import { isGitIgnoredDir } from '@/utils/git-ignored';
2324

2425
/** One rejected or unanalysable file. */
2526
interface SkippedPythonFile {
@@ -580,7 +581,7 @@ export class PythonProjectAnalyzer {
580581
for (const entry of entries) {
581582
const full = path.join(dir, entry.name);
582583
if (entry.isDirectory()) {
583-
if (excludes.has(entry.name) || entry.name.endsWith('.egg-info')) {
584+
if (excludes.has(entry.name) || entry.name.endsWith('.egg-info') || isGitIgnoredDir(full)) {
584585
continue;
585586
}
586587
if (isRoot && entry.name === 'build') {

‎parser/src/workflows/services/services-project-analyzer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SkippedFileReason } from '@/enums/SkippedFileReason';
1818
import { ServicesParser } from '@/parsers/services/services-parser';
1919
import { ProjectInfo } from '@/types/ProjectInfo';
2020
import { EntityUtils } from '@/utils/entity-utils';
21+
import { isGitIgnoredDir } from '@/utils/git-ignored';
2122

2223
interface SkippedServicesFile {
2324
filePath: string;
@@ -215,7 +216,7 @@ export class ServicesProjectAnalyzer {
215216
continue;
216217
}
217218

218-
if (EXCLUDED_DIRS.has(entry.name) || entry.name.startsWith('.')) {
219+
if (EXCLUDED_DIRS.has(entry.name) || entry.name.startsWith('.') || isGitIgnoredDir(subPath)) {
219220
continue;
220221
}
221222

0 commit comments

Comments
 (0)