Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ target/
!graph/test/java/cases/62-build-output-is-not-source/src/**/target/**
!graph/test/java/cases/62-build-output-is-not-source/src/**/build/
!graph/test/java/cases/62-build-output-is-not-source/src/**/build/**
# ...and the Gradle case: build/ beside a build script is the construct (the parser must skip it by that
# anchor, not because git ignores it), and src/main/java/app/build/ is the control.
!graph/test/java/cases/64-gradle-build-output-is-not-source/src/**/build/
!graph/test/java/cases/64-gradle-build-output-is-not-source/src/**/build/**

# ── Secrets & local configuration ────────────────────────────────────────────
.env
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id 'java'
}

dependencies {
implementation project(':lib')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.gen;

// Written by an annotation processor into Gradle's output directory: not the project's source.
// Its only call is to the JDK, so the bytecode oracle scores nothing here; if the parser read
// this file, the .edges golden would gain its boundary_lib row.
public class GeneratedStage {
public String label(String name) {
return name.trim();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
`java-library`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.gen;

// A Gradle MODULE's output (beside lib/build.gradle.kts): skipped like the root project's.
public class GeneratedLibStep {
public String label(String name) {
return name.strip();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.lib.build;

// The same inside a Gradle module: lib/src/main/java/app/lib/build is a package, not lib/build.
public class LibStep {
public String apply(String name) {
return name + "-applied";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rootProject.name = 'app'
include 'lib'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app;

import app.build.StageBuilder;
import app.lib.build.LibStep;

public class Pipeline {
public String run(String name) {
String staged = new StageBuilder().stage(name);
return new LibStep().apply(staged);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.build;

// A source package named `build`, in a Gradle project: it has no build script beside it, so it is source.
public class StageBuilder {
public String stage(String name) {
return name + "-staged";
}
}
4 changes: 2 additions & 2 deletions graph/test/java/expected/62-build-output-is-not-source.edges
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ambiguous_unknown method app.WidgetValidator#check(String) -> -
ambiguous_unknown new app.WidgetValidator#check(String) -> -
boundary_lib method app.WidgetValidator#check(String) -> external:String.isEmpty
known_edge method app.WidgetValidator#check(String) -> app.build.StepBuilder#next(String)
known_edge method app.WidgetValidator#check(String) -> app.site.PageRenderer#render(String)
known_edge new app.WidgetValidator#check(String) -> app.build.StepBuilder#<init>()
known_edge new app.WidgetValidator#check(String) -> app.site.PageRenderer#<init>()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oracle=4 engine=4 agree=4 missing=0 (known 0, NEW 0) extra=0
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
ambiguous_unknown METHOD_PARAM 0 app.WidgetValidator#check(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_PARAM 0 app.build.StepBuilder#next(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_PARAM 0 app.site.PageRenderer#render(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_RETURN 0 app.build.StepBuilder#next(String) [METHOD] -> -
ambiguous_unknown METHOD_RETURN 0 app.site.PageRenderer#render(String) [METHOD] -> -
ambiguous_unknown OBJECT_CREATION_TYPE 0 app.WidgetValidator#check(String) [EXPRESSION] -> -
known_edge OBJECT_CREATION_TYPE 0 app.WidgetValidator#check(String) [EXPRESSION] -> app.build.StepBuilder
known_edge OBJECT_CREATION_TYPE 0 app.WidgetValidator#check(String) [EXPRESSION] -> app.site.PageRenderer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oracle=1 engine=1 agree=1 missing=0 (known 0, NEW 0) extra=0
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
known_edge method app.Pipeline#run(String) -> app.build.StageBuilder#stage(String)
known_edge method app.Pipeline#run(String) -> app.lib.build.LibStep#apply(String)
known_edge new app.Pipeline#run(String) -> app.build.StageBuilder#<init>()
known_edge new app.Pipeline#run(String) -> app.lib.build.LibStep#<init>()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oracle=4 engine=4 agree=4 missing=0 (known 0, NEW 0) extra=0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ambiguous_unknown LOCAL_VARIABLE 0 app.Pipeline#run(String) [LOCAL_VARIABLE] -> -
ambiguous_unknown METHOD_PARAM 0 app.Pipeline#run(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_PARAM 0 app.build.StageBuilder#stage(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_PARAM 0 app.lib.build.LibStep#apply(String) [METHOD_PARAM] -> -
ambiguous_unknown METHOD_RETURN 0 app.Pipeline#run(String) [METHOD] -> -
ambiguous_unknown METHOD_RETURN 0 app.build.StageBuilder#stage(String) [METHOD] -> -
ambiguous_unknown METHOD_RETURN 0 app.lib.build.LibStep#apply(String) [METHOD] -> -
known_edge OBJECT_CREATION_TYPE 0 app.Pipeline#run(String) [EXPRESSION] -> app.build.StageBuilder
known_edge OBJECT_CREATION_TYPE 0 app.Pipeline#run(String) [EXPRESSION] -> app.lib.build.LibStep
2 changes: 1 addition & 1 deletion graph/test/java/expected/oracle-agreement.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cases compared 48 agreeing 48
cases compared 49 agreeing 49
CONSTRUCTOR-RULE disagreements: 0 cases, 0 rows
OTHER disagreements: 0
rows naming no method: 0
Expand Down
4 changes: 3 additions & 1 deletion parser/src/constants/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import * as path from 'path';
export const HASH_ALGO = 'md5';

/**
* Directories to exclude when scanning projects or source files
* Directories to exclude when scanning projects or source files.
* The Java source walk does not skip `build` by this name: only a Gradle project's output
* (utils/generated-output.ts isGradleBuildOutput), since `build` is also a Java package.
*/
export const EXCLUDED_DIRS = new Set([
'node_modules',
Expand Down
22 changes: 22 additions & 0 deletions parser/src/utils/generated-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,25 @@ export function isGeneratedOutputDirectory(parent: string, name: string): boolea
|| (fs.existsSync(path.join(directory, 'navigation.html'))
&& fs.existsSync(path.join(directory, 'scripts', 'sourceset_dependencies.js')));
}

/**
* The files that make a directory the root of a Gradle project or module. Gradle writes that
* project's output (compiled classes, processed resources, and generated sources such as
* `build/generated/sources/annotationProcessor/java/main`) into a `build` directory beside them.
*/
export const GRADLE_BUILD_FILES = ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts'] as const;

/**
* Is `parent/name` a Gradle project's output directory, whose `.java` files a build wrote?
*
* Anchored on its owner, the way `target` is anchored on a `pom.xml` above: a `build` beside a
* Gradle build or settings script. `build` is also an ordinary Java package name (builders, build
* steps: `src/main/java/app/build/StepBuilder.java`), and pruning every directory of that name
* dropped such a package from the graph with no skipped-file row to say so. A `build` inside a
* source tree or a package path has no build script beside it, so it is walked.
* Used by the Java source walk (java-project-analyzer.ts); the refresher's file table
* (ax_fresh.py `gradle_output`) applies the same rule, so freshness and the language count agree.
*/
export function isGradleBuildOutput(parent: string, name: string): boolean {
return name === 'build' && GRADLE_BUILD_FILES.some((file) => fs.existsSync(path.join(parent, file)));
}
8 changes: 7 additions & 1 deletion parser/src/workflows/java/java-project-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TypeAnnotation } from '@/analysis-types/java/TypeAnnotation';
import { TypeParameter } from '@/analysis-types/java/TypeParameter';
import { TypeReference } from '@/analysis-types/java/TypeReference';
import { TypeRegistry } from '@/analysis-types/java/TypeRegistry';
import { isGradleBuildOutput } from '@/utils/generated-output';
import { EXCLUDED_DIRS, isJavaTestDir, ANALYSIS_OUTPUT_DIR, OUTPUT_TYPE_REGISTRY_CSV_FILENAME, OUTPUT_TYPE_PARAMETER_CSV_FILENAME, OUTPUT_TYPE_REFERENCE_CSV_FILENAME, OUTPUT_TYPE_ANNOTATION_CSV_FILENAME, OUTPUT_ANNOTATION_ARGUMENT_CSV_FILENAME, OUTPUT_METHOD_REGISTRY_CSV_FILENAME, OUTPUT_METHOD_PARAMETER_CSV_FILENAME, OUTPUT_METHOD_TYPE_PARAMETER_CSV_FILENAME, OUTPUT_ENUM_CONSTANT_CSV_FILENAME, OUTPUT_FIELD_REGISTRY_CSV_FILENAME, OUTPUT_FIELD_POSITION_CSV_FILENAME, OUTPUT_IMPORT_REGISTRY_CSV_FILENAME, OUTPUT_MODULE_REGISTRY_CSV_FILENAME, OUTPUT_MODULE_DIRECTIVE_CSV_FILENAME, OUTPUT_EXPRESSION_REFERENCE_CSV_FILENAME, OUTPUT_LOCAL_VARIABLE_REGISTRY_CSV_FILENAME, OUTPUT_BLOCK_REGISTRY_CSV_FILENAME, OUTPUT_COMMENT_REGISTRY_CSV_FILENAME, OUTPUT_SKIPPED_JAVA_FILES_CSV_FILENAME, JAVA_ENTITY_TYPES, FILE_EXTENSIONS, LARGE_FILE_LINE_THRESHOLD } from '@/constants/consts';
import { ENTITY_IDENTIFIERS } from '@/constants/entity-constants';
import { SkippedFileReason } from '@/enums/SkippedFileReason';
Expand Down Expand Up @@ -321,7 +322,12 @@ export class JavaProjectAnalyzer {
for (const entry of entries) {
if (entry.isDirectory()) {
const isTestDir = isJavaTestDir(entry.name);
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.') && !(excludeTests && isTestDir)
// `build` is skipped only as a Gradle project's output, never by name: it is also a
// Java package name (see isGradleBuildOutput).
const excluded = entry.name === 'build'
? isGradleBuildOutput(dirPath, entry.name)
: EXCLUDED_DIRS.has(entry.name);
if (!excluded && !entry.name.startsWith('.') && !(excludeTests && isTestDir)
&& !isGitIgnoredDir(path.join(dirPath, entry.name))) {
const subPath = path.join(dirPath, entry.name);
await this.scanForJavaFiles(subPath, files, excludeTests);
Expand Down
15 changes: 13 additions & 2 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
# reads: a directory pruned here that the parser DOES read is an edit no refresh ever sees and that `index` calls up to
# date, which is what one shared list did to `out`, `build`, `target` and `coverage` in Python and C# (and `coverage`
# in Java). A directory watched here that the parser skips costs only a needless rebuild. Each set is the parser's own:
# java parser/src/constants/consts.ts EXCLUDED_DIRS, and every directory whose name starts with a dot
# java parser/src/constants/consts.ts EXCLUDED_DIRS except `build`, and every directory whose name starts with
# a dot; a `build` directory is skipped only as a Gradle project's output (gradle_output below)
# typescript parser/src/constants/typescript-constants.ts TS_SKIP_DIRECTORIES, and dot directories
# javascript parser/src/constants/javascript-constants.ts JS_SKIP_DIRECTORIES
# python parser/src/workflows/python/python-project-analyzer.ts DEFAULT_EXCLUDES, `*.egg-info`, and under a
Expand All @@ -75,7 +76,7 @@
# .axiomcode (the graph's own directory) and .git are pruned for every language: the build puts .axiomcode in
# .git/info/exclude, so the parser skips it too. Directories git ignores are pruned for every language (git_ignored_dirs).
SKIP = {
'java': frozenset({'node_modules', '.git', '.idea', '.vscode', 'dist', 'build', 'target', 'out', '__pycache__',
'java': frozenset({'node_modules', '.git', '.idea', '.vscode', 'dist', 'target', 'out', '__pycache__',
'.pytest_cache', 'venv', 'env'}),
'typescript': frozenset({'node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.next', '.nuxt', '.turbo',
'.cache', '.yarn', 'bower_components'}),
Expand Down Expand Up @@ -112,6 +113,15 @@ def generated_output(parent, name):
return os.path.isfile(os.path.join(d, 'element-list')) or os.path.isfile(os.path.join(d, 'package-list')) or \
(os.path.isfile(os.path.join(d, 'navigation.html')) and os.path.isfile(os.path.join(d, 'scripts', 'sourceset_dependencies.js')))

# A GRADLE PROJECT'S OUTPUT. The Java parser skips a directory named `build` only when a Gradle build or settings
# script sits beside it (parser/src/utils/generated-output.ts isGradleBuildOutput): `build` is also a Java package name,
# and one inside a source tree is read, so an edit there must be watched and its files counted.
GRADLE_BUILD_FILES = ('build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts')

def gradle_output(parent, name):
"""True when `parent`/`name` is the `build` directory a Gradle project or module writes its output into"""
return name == 'build' and any(os.path.isfile(os.path.join(parent, f)) for f in GRADLE_BUILD_FILES)

def out_dir(repo): return os.path.join(repo, '.axiomcode', 'out')
def table_path(repo): return os.path.join(out_dir(repo), 'files.json')
def state_path(repo): return os.path.join(repo, '.axiomcode', 'refresh.json')
Expand Down Expand Up @@ -170,6 +180,7 @@ def watched(root, lang):
if ignored and os.path.normpath(os.path.join(rd, s)) in ignored: continue
o = off | {l for l in langs if prunes(l, s, parent)}
if web and not web <= o and generated_output(d, s): o = o | web # a build's output (#1545)
if 'java' in langs and 'java' not in o and gradle_output(d, s): o = o | {'java'} # Gradle's output
if all(l in o for l in langs): continue
keep.append(s); hidden[os.path.join(d, s)] = o
subdirs[:] = keep
Expand Down
10 changes: 7 additions & 3 deletions tests/freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def ts_list(rel, name):

def prune_checks():
parser = {
'java': ts_list('constants/consts.ts', 'EXCLUDED_DIRS'),
# the Java walk skips `build` only beside a Gradle build script (isGradleBuildOutput), never by name
'java': ts_list('constants/consts.ts', 'EXCLUDED_DIRS') - {'build'},
'typescript': ts_list('constants/typescript-constants.ts', 'TS_SKIP_DIRECTORIES'),
'javascript': ts_list('constants/javascript-constants.ts', 'JS_SKIP_DIRECTORIES'),
'python': ts_list('workflows/python/python-project-analyzer.ts', 'DEFAULT_EXCLUDES'),
Expand All @@ -70,13 +71,16 @@ def prune_checks():
'python': ['app/out/calc.py', 'app/build/calc.py', 'target/t.py', 'coverage/c.py', 'build/lib/gen.py',
'build/frontend.py', '.venv/v.py', 'pkg.egg-info/e.py', 'dist/d.py', 'app/ok.py'],
'java': ['src/main/java/app/coverage/Calc.java', 'src/main/java/app/Ok.java', 'out/O.java', 'target/T.java',
'build/B.java', '.hidden/H.java'],
'build/B.java', '.hidden/H.java', 'src/main/java/app/build/StepBuilder.java',
'mod/build.gradle.kts', 'mod/build/generated/sources/annotationProcessor/java/main/app/Gen.java',
'mod/src/main/java/app/build/Step.java'],
'csharp': ['src/Shop/build/Calc.cs', 'src/Shop/out/O.cs', 'target/T.cs', 'coverage/C.cs', 'src/Shop/Ok.cs',
'src/Shop/obj/G.cs', 'bin/B.cs', 'packages/P.cs'],
}
read = { # what the parser reads (True) or skips (False), by its own lists
'python': [True, True, True, True, False, True, False, False, False, True],
'java': [True, True, False, False, False, False],
# a `build` package is read; only a Gradle module's build/ (beside mod/build.gradle.kts, itself watched) is skipped
'java': [True, True, False, False, True, False, True, True, False, True],
'csharp': [True, True, True, True, True, False, False, False],
}
for lang, files in tree.items():
Expand Down
Loading