diff --git a/.gitignore b/.gitignore index 58205348b..8568550de 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build.gradle b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build.gradle new file mode 100644 index 000000000..3a0ee1153 --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(':lib') +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedStage.java b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedStage.java new file mode 100644 index 000000000..f58ff966b --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedStage.java @@ -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(); + } +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build.gradle.kts b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build.gradle.kts new file mode 100644 index 000000000..f6727627a --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + `java-library` +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedLibStep.java b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedLibStep.java new file mode 100644 index 000000000..280d007ef --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/build/generated/sources/annotationProcessor/java/main/app/gen/GeneratedLibStep.java @@ -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(); + } +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/src/main/java/app/lib/build/LibStep.java b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/src/main/java/app/lib/build/LibStep.java new file mode 100644 index 000000000..352c08b93 --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/lib/src/main/java/app/lib/build/LibStep.java @@ -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"; + } +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/settings.gradle b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/settings.gradle new file mode 100644 index 000000000..e325b7ebc --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'app' +include 'lib' diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/Pipeline.java b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/Pipeline.java new file mode 100644 index 000000000..9b2364cfd --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/Pipeline.java @@ -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); + } +} diff --git a/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/build/StageBuilder.java b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/build/StageBuilder.java new file mode 100644 index 000000000..7ac14a55e --- /dev/null +++ b/graph/test/java/cases/64-gradle-build-output-is-not-source/src/src/main/java/app/build/StageBuilder.java @@ -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"; + } +} diff --git a/graph/test/java/expected/62-build-output-is-not-source.edges b/graph/test/java/expected/62-build-output-is-not-source.edges index 9768acfde..1779c8a8c 100644 --- a/graph/test/java/expected/62-build-output-is-not-source.edges +++ b/graph/test/java/expected/62-build-output-is-not-source.edges @@ -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#() known_edge new app.WidgetValidator#check(String) -> app.site.PageRenderer#() diff --git a/graph/test/java/expected/62-build-output-is-not-source.oracle b/graph/test/java/expected/62-build-output-is-not-source.oracle new file mode 100644 index 000000000..518b0e5c3 --- /dev/null +++ b/graph/test/java/expected/62-build-output-is-not-source.oracle @@ -0,0 +1 @@ +oracle=4 engine=4 agree=4 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/java/expected/62-build-output-is-not-source.type-use b/graph/test/java/expected/62-build-output-is-not-source.type-use index 6661dc348..237028423 100644 --- a/graph/test/java/expected/62-build-output-is-not-source.type-use +++ b/graph/test/java/expected/62-build-output-is-not-source.type-use @@ -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 diff --git a/graph/test/java/expected/63-javadoc-root-is-not-a-project.oracle b/graph/test/java/expected/63-javadoc-root-is-not-a-project.oracle new file mode 100644 index 000000000..97a1dbf27 --- /dev/null +++ b/graph/test/java/expected/63-javadoc-root-is-not-a-project.oracle @@ -0,0 +1 @@ +oracle=1 engine=1 agree=1 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/java/expected/64-gradle-build-output-is-not-source.edges b/graph/test/java/expected/64-gradle-build-output-is-not-source.edges new file mode 100644 index 000000000..7065eed6f --- /dev/null +++ b/graph/test/java/expected/64-gradle-build-output-is-not-source.edges @@ -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#() +known_edge new app.Pipeline#run(String) -> app.lib.build.LibStep#() diff --git a/graph/test/java/expected/64-gradle-build-output-is-not-source.oracle b/graph/test/java/expected/64-gradle-build-output-is-not-source.oracle new file mode 100644 index 000000000..518b0e5c3 --- /dev/null +++ b/graph/test/java/expected/64-gradle-build-output-is-not-source.oracle @@ -0,0 +1 @@ +oracle=4 engine=4 agree=4 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/java/expected/64-gradle-build-output-is-not-source.type-use b/graph/test/java/expected/64-gradle-build-output-is-not-source.type-use new file mode 100644 index 000000000..f7c8f9094 --- /dev/null +++ b/graph/test/java/expected/64-gradle-build-output-is-not-source.type-use @@ -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 diff --git a/graph/test/java/expected/oracle-agreement.txt b/graph/test/java/expected/oracle-agreement.txt index bad554eae..4bb656bac 100644 --- a/graph/test/java/expected/oracle-agreement.txt +++ b/graph/test/java/expected/oracle-agreement.txt @@ -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 diff --git a/parser/src/constants/consts.ts b/parser/src/constants/consts.ts index 5142f59a0..000999d09 100644 --- a/parser/src/constants/consts.ts +++ b/parser/src/constants/consts.ts @@ -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', diff --git a/parser/src/utils/generated-output.ts b/parser/src/utils/generated-output.ts index 3f2176c2d..02a169304 100644 --- a/parser/src/utils/generated-output.ts +++ b/parser/src/utils/generated-output.ts @@ -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))); +} diff --git a/parser/src/workflows/java/java-project-analyzer.ts b/parser/src/workflows/java/java-project-analyzer.ts index 118e4c82e..625e8880a 100644 --- a/parser/src/workflows/java/java-project-analyzer.ts +++ b/parser/src/workflows/java/java-project-analyzer.ts @@ -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'; @@ -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); diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py index bcb55b70e..8d334ddc8 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py @@ -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 @@ -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'}), @@ -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') @@ -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 diff --git a/tests/freshness.py b/tests/freshness.py index 6f190cb71..16aafe008 100644 --- a/tests/freshness.py +++ b/tests/freshness.py @@ -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'), @@ -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():