diff --git a/.gitignore b/.gitignore
index 7e9fac2e7..58205348b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -154,6 +154,13 @@ replay_pid*.log
target/
.gradle/
.mvn/wrapper/maven-wrapper.jar
+# ...EXCEPT the Java case that tests build-output exclusion itself (#1545). Its target/ is the construct, and
+# app/build/ and src/main/js/target/ are the controls: real source in a directory named like a build's. Placed
+# after both blankets (build/ above, target/ here), since gitignore is last-match-wins.
+!graph/test/java/cases/62-build-output-is-not-source/src/**/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/**
# ── Secrets & local configuration ────────────────────────────────────────────
.env
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/element-list b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/element-list
new file mode 100644
index 000000000..992ed3eb0
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/element-list
@@ -0,0 +1,3 @@
+app
+app.build
+app.site
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/index.html b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/index.html
new file mode 100644
index 000000000..e95a1275c
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/index.html
@@ -0,0 +1,2 @@
+
+
API
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/script.js b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/script.js
new file mode 100644
index 000000000..d3c7a9bcb
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/script.js
@@ -0,0 +1,8 @@
+var moduleSearchIndex;
+function loadScripts(doc, tag) {
+ createElem(doc, tag, 'search.js');
+}
+function createElem(doc, tag, path) {
+ var script = doc.createElement(tag);
+ script.src = path;
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/search.js b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/search.js
new file mode 100644
index 000000000..a75692169
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/apidocs/search.js
@@ -0,0 +1,3 @@
+function doSearch(term) {
+ return term.trim();
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/index.html b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/index.html
new file mode 100644
index 000000000..fdbaf226f
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/index.html
@@ -0,0 +1,2 @@
+
+widgets
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/navigation.html b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/navigation.html
new file mode 100644
index 000000000..5154674ea
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/navigation.html
@@ -0,0 +1 @@
+
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/main.js b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/main.js
new file mode 100644
index 000000000..d868c26db
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/main.js
@@ -0,0 +1,3 @@
+function toggleMenu(button) {
+ return button.classList.toggle("open");
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/sourceset_dependencies.js b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/sourceset_dependencies.js
new file mode 100644
index 000000000..7952b4ac0
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/kdoc/scripts/sourceset_dependencies.js
@@ -0,0 +1 @@
+sourceset_dependencies = '{":widgets/main":[]}'
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/index.html b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/index.html
new file mode 100644
index 000000000..e95a1275c
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/index.html
@@ -0,0 +1,2 @@
+
+API
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/package-list b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/package-list
new file mode 100644
index 000000000..992ed3eb0
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/package-list
@@ -0,0 +1,3 @@
+app
+app.build
+app.site
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/script.js b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/script.js
new file mode 100644
index 000000000..d3c7a9bcb
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/docs/legacy-api/script.js
@@ -0,0 +1,8 @@
+var moduleSearchIndex;
+function loadScripts(doc, tag) {
+ createElem(doc, tag, 'search.js');
+}
+function createElem(doc, tag, path) {
+ var script = doc.createElement(tag);
+ script.src = path;
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/package.json b/graph/test/java/cases/62-build-output-is-not-source/src/package.json
new file mode 100644
index 000000000..ab544d59f
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/package.json
@@ -0,0 +1 @@
+{ "name": "widgets-web", "version": "1.0.0", "private": true, "main": "src/main/js/app.js" }
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/pom.xml b/graph/test/java/cases/62-build-output-is-not-source/src/pom.xml
new file mode 100644
index 000000000..52c358a57
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/pom.xml
@@ -0,0 +1,7 @@
+
+
+ 4.0.0
+ example
+ widgets
+ 1.0
+
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/WidgetValidator.java b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/WidgetValidator.java
new file mode 100644
index 000000000..7ac4640be
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/WidgetValidator.java
@@ -0,0 +1,12 @@
+package app;
+
+import app.build.StepBuilder;
+import app.site.PageRenderer;
+
+public class WidgetValidator {
+ public boolean check(String name) {
+ new PageRenderer().render(name);
+ new StepBuilder().next(name);
+ return name != null && !name.isEmpty();
+ }
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/build/StepBuilder.java b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/build/StepBuilder.java
new file mode 100644
index 000000000..35b5f6274
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/build/StepBuilder.java
@@ -0,0 +1,7 @@
+package app.build;
+
+public class StepBuilder {
+ public String next(String name) {
+ return name + "-next";
+ }
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/site/PageRenderer.java b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/site/PageRenderer.java
new file mode 100644
index 000000000..04b248bfc
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/java/app/site/PageRenderer.java
@@ -0,0 +1,7 @@
+package app.site;
+
+public class PageRenderer {
+ public String render(String name) {
+ return "" + name + "
";
+ }
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/app.js b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/app.js
new file mode 100644
index 000000000..17e711b7f
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/app.js
@@ -0,0 +1,7 @@
+const aim = require('./target/aim');
+
+function start(order) {
+ return aim.point(order);
+}
+
+module.exports = { start };
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/target/aim.js b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/target/aim.js
new file mode 100644
index 000000000..5e6383229
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/js/target/aim.js
@@ -0,0 +1,5 @@
+function point(order) {
+ return order;
+}
+
+module.exports = { point };
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/index.html b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/index.html
new file mode 100644
index 000000000..b11030fb3
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/main.js b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/main.js
new file mode 100644
index 000000000..9803e2465
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/src/main/resources/static/main.js
@@ -0,0 +1,3 @@
+function showWidgets(list) {
+ return list.length;
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/target/classes/static/bundle.js b/graph/test/java/cases/62-build-output-is-not-source/src/target/classes/static/bundle.js
new file mode 100644
index 000000000..0e7cd8f53
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/target/classes/static/bundle.js
@@ -0,0 +1,3 @@
+function bundledStart(order) {
+ return order;
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/target/site/apidocs/script.js b/graph/test/java/cases/62-build-output-is-not-source/src/target/site/apidocs/script.js
new file mode 100644
index 000000000..d3c7a9bcb
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/target/site/apidocs/script.js
@@ -0,0 +1,8 @@
+var moduleSearchIndex;
+function loadScripts(doc, tag) {
+ createElem(doc, tag, 'search.js');
+}
+function createElem(doc, tag, path) {
+ var script = doc.createElement(tag);
+ script.src = path;
+}
diff --git a/graph/test/java/cases/62-build-output-is-not-source/src/webpack.config.js b/graph/test/java/cases/62-build-output-is-not-source/src/webpack.config.js
new file mode 100644
index 000000000..67355c0df
--- /dev/null
+++ b/graph/test/java/cases/62-build-output-is-not-source/src/webpack.config.js
@@ -0,0 +1 @@
+module.exports = { entry: './src/main/js/app.js' };
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/element-list b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/element-list
new file mode 100644
index 000000000..992ed3eb0
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/element-list
@@ -0,0 +1,3 @@
+app
+app.build
+app.site
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/index.html b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/index.html
new file mode 100644
index 000000000..e95a1275c
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/index.html
@@ -0,0 +1,2 @@
+
+API
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script-dir/jquery.min.js b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script-dir/jquery.min.js
new file mode 100644
index 000000000..7ff50858e
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script-dir/jquery.min.js
@@ -0,0 +1,3 @@
+function jqueryReady(fn) {
+ return fn();
+}
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script.js b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script.js
new file mode 100644
index 000000000..d3c7a9bcb
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/script.js
@@ -0,0 +1,8 @@
+var moduleSearchIndex;
+function loadScripts(doc, tag) {
+ createElem(doc, tag, 'search.js');
+}
+function createElem(doc, tag, path) {
+ var script = doc.createElement(tag);
+ script.src = path;
+}
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/search.js b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/search.js
new file mode 100644
index 000000000..a75692169
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/apidocs/search.js
@@ -0,0 +1,3 @@
+function doSearch(term) {
+ return term.trim();
+}
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/demo.js b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/demo.js
new file mode 100644
index 000000000..a75243f98
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/demo.js
@@ -0,0 +1,7 @@
+function start(widget) {
+ return render(widget);
+}
+
+function render(widget) {
+ return widget;
+}
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/index.html b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/index.html
new file mode 100644
index 000000000..1639054e6
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/demo/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/notes.js b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/notes.js
new file mode 100644
index 000000000..cc93ad677
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/notes.js
@@ -0,0 +1,3 @@
+function notes() {
+ return 1;
+}
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/package-list b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/package-list
new file mode 100644
index 000000000..b80f0bd60
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/docs/notes/package-list
@@ -0,0 +1 @@
+app
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/pom.xml b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/pom.xml
new file mode 100644
index 000000000..52c358a57
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/pom.xml
@@ -0,0 +1,7 @@
+
+
+ 4.0.0
+ example
+ widgets
+ 1.0
+
diff --git a/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/src/main/java/app/Widget.java b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/src/main/java/app/Widget.java
new file mode 100644
index 000000000..73aad76a3
--- /dev/null
+++ b/graph/test/java/cases/63-javadoc-root-is-not-a-project/src/src/main/java/app/Widget.java
@@ -0,0 +1,11 @@
+package app;
+
+public class Widget {
+ public String name() {
+ return label("w");
+ }
+
+ String label(String s) {
+ return s.trim();
+ }
+}
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
new file mode 100644
index 000000000..9768acfde
--- /dev/null
+++ b/graph/test/java/expected/62-build-output-is-not-source.edges
@@ -0,0 +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.site.PageRenderer#render(String)
+known_edge new app.WidgetValidator#check(String) -> app.site.PageRenderer#()
diff --git a/graph/test/java/expected/62-build-output-is-not-source.other-languages b/graph/test/java/expected/62-build-output-is-not-source.other-languages
new file mode 100644
index 000000000..f696ed0ac
--- /dev/null
+++ b/graph/test/java/expected/62-build-output-is-not-source.other-languages
@@ -0,0 +1,8 @@
+javascript module src/main/js/app.js
+javascript module src/main/js/target/aim.js
+javascript module src/main/resources/static/main.js
+javascript module webpack.config.js
+javascript pruned docs/apidocs 2 JavaScript file(s) under a build's output directory (apidocs)
+javascript pruned docs/kdoc 2 JavaScript file(s) under a build's output directory (kdoc)
+javascript pruned docs/legacy-api 1 JavaScript file(s) under a build's output directory (legacy-api)
+javascript pruned target 2 JavaScript file(s) under a build's output directory (target)
diff --git a/graph/test/java/expected/62-build-output-is-not-source.other-languages-assert b/graph/test/java/expected/62-build-output-is-not-source.other-languages-assert
new file mode 100644
index 000000000..7818fe6c0
--- /dev/null
+++ b/graph/test/java/expected/62-build-output-is-not-source.other-languages-assert
@@ -0,0 +1,20 @@
+# #1545: a build's output is not source, and a directory merely NAMED like one still is.
+# Maven's target/ (a pom.xml beside it), a javadoc output directory (index.html with element-list or
+# package-list) and a Dokka one (index.html with navigation.html and scripts/sourceset_dependencies.js) hold no
+# project JavaScript:
+- javascript module target/site/apidocs/script.js
+- javascript module target/classes/static/bundle.js
+- javascript module docs/apidocs/script.js
+- javascript module docs/apidocs/search.js
+- javascript module docs/legacy-api/script.js
+- javascript module docs/kdoc/scripts/main.js
+- javascript module docs/kdoc/scripts/sourceset_dependencies.js
+# the controls: the project's own JavaScript, a JavaScript directory named target with no pom.xml beside it,
+# and a static folder with an index.html that is not a javadoc:
++ javascript module webpack.config.js
++ javascript module src/main/js/app.js
++ javascript module src/main/js/target/aim.js
++ javascript module src/main/resources/static/main.js
+# Not asserted here, and left as it was: app/build/StepBuilder.java is a Java package named build, and the Java
+# walk still prunes every directory of that name (#531). The two ambiguous_unknown rows in the .edges golden are
+# that call and that `new`; a fix for #531 turns them into known_edge rows.
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
new file mode 100644
index 000000000..6661dc348
--- /dev/null
+++ b/graph/test/java/expected/62-build-output-is-not-source.type-use
@@ -0,0 +1,5 @@
+ambiguous_unknown METHOD_PARAM 0 app.WidgetValidator#check(String) [METHOD_PARAM] -> -
+ambiguous_unknown METHOD_PARAM 0 app.site.PageRenderer#render(String) [METHOD_PARAM] -> -
+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.site.PageRenderer
diff --git a/graph/test/java/expected/63-javadoc-root-is-not-a-project.edges b/graph/test/java/expected/63-javadoc-root-is-not-a-project.edges
new file mode 100644
index 000000000..e146aafb9
--- /dev/null
+++ b/graph/test/java/expected/63-javadoc-root-is-not-a-project.edges
@@ -0,0 +1,2 @@
+boundary_lib method app.Widget#label(String) -> external:String.trim
+known_edge method app.Widget#name() -> app.Widget#label(String)
diff --git a/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages b/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages
new file mode 100644
index 000000000..37bc76e34
--- /dev/null
+++ b/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages
@@ -0,0 +1,2 @@
+javascript module docs/demo/demo.js
+javascript module docs/notes/notes.js
diff --git a/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages-assert b/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages-assert
new file mode 100644
index 000000000..f178791c0
--- /dev/null
+++ b/graph/test/java/expected/63-javadoc-root-is-not-a-project.other-languages-assert
@@ -0,0 +1,11 @@
+# #1545: a javadoc committed at docs/apidocs/ in a repository with no package.json. The project scan found its loose
+# scripts and registered the javadoc directory itself as a JavaScript project root, and a walk never tests its own
+# root for being a build's output, so the pruning in the walks never saw it. Neither it nor a directory under it
+# (script-dir/, where javadoc puts jQuery) may be a project:
+- javascript module docs/apidocs/script.js
+- javascript module docs/apidocs/search.js
+- javascript module docs/apidocs/script-dir/jquery.min.js
+# the near-miss controls, each a loose-script directory that is NOT a javadoc: one with an index.html and no
+# element-list or package-list, one with a package-list and no index.html:
++ javascript module docs/demo/demo.js
++ javascript module docs/notes/notes.js
diff --git a/graph/test/java/expected/63-javadoc-root-is-not-a-project.type-use b/graph/test/java/expected/63-javadoc-root-is-not-a-project.type-use
new file mode 100644
index 000000000..eab4ae779
--- /dev/null
+++ b/graph/test/java/expected/63-javadoc-root-is-not-a-project.type-use
@@ -0,0 +1,3 @@
+ambiguous_unknown METHOD_PARAM 0 app.Widget#label(String) [METHOD_PARAM] -> -
+ambiguous_unknown METHOD_RETURN 0 app.Widget#label(String) [METHOD] -> -
+ambiguous_unknown METHOD_RETURN 0 app.Widget#name() [METHOD] -> -
diff --git a/graph/test/java/expected/oracle-agreement.txt b/graph/test/java/expected/oracle-agreement.txt
index 22a372c44..bad554eae 100644
--- a/graph/test/java/expected/oracle-agreement.txt
+++ b/graph/test/java/expected/oracle-agreement.txt
@@ -1,4 +1,4 @@
-cases compared 46 agreeing 46
+cases compared 48 agreeing 48
CONSTRUCTOR-RULE disagreements: 0 cases, 0 rows
OTHER disagreements: 0
rows naming no method: 0
diff --git a/graph/test/java/run-tests.sh b/graph/test/java/run-tests.sh
index 53541cb2a..b2556b431 100755
--- a/graph/test/java/run-tests.sh
+++ b/graph/test/java/run-tests.sh
@@ -437,6 +437,47 @@ for dir in "$HERE"/cases/*/; do
cfg_summary="$cfg_summary [remote: ${rem_rows}]"
fi
+ # ── OTHER-LANGUAGES golden (#1545) ────────────────────────────────────────
+ # The same parse runs the JavaScript and TypeScript analyzers over the case, and no Java golden reads what they
+ # took: a Maven build's target/ and a generated javadoc were extracted as a JavaScript project, a second graph was
+ # built from javadoc's own script.js, and every golden here stayed green. One line per module and per pruned
+ # directory, written only when a case has any, so the cases with no other language are unaffected.
+ # expected/.other-languages-assert names the rows that matter (`+ ` must be present, `- ` must
+ # not), checked even under --bless, because a golden pins a file that must NOT be source only by its absence.
+ python3 "$HERE/tools/normalize_other_languages.py" "$w/ir" > "$w/actual.other-languages" 2>"$w/other-languages.log" || {
+ echo "FAIL (other-languages report — see $w/other-languages.log)"; fail=$((fail+1)); failed+=("$name"); continue; }
+ ol_rows=$(grep -c . "$w/actual.other-languages" || true)
+ oexp2="$HERE/expected/$name.other-languages"
+ if [ "$BLESS" = "1" ]; then
+ if [ "${ol_rows:-0}" -gt 0 ]; then cp "$w/actual.other-languages" "$oexp2"; else rm -f "$oexp2"; fi
+ elif [ -f "$oexp2" ] || [ "${ol_rows:-0}" -gt 0 ]; then
+ if [ ! -f "$oexp2" ]; then
+ echo "FAIL (other-language rows but no golden — run with --bless)"; fail=$((fail+1)); failed+=("$name"); continue; fi
+ if ! diff -q "$oexp2" "$w/actual.other-languages" >/dev/null; then
+ echo "FAIL (other languages changed)"; diff -u "$oexp2" "$w/actual.other-languages" | sed 's/^/ /' | head -30
+ fail=$((fail+1)); failed+=("$name"); continue; fi
+ cfg_summary="$cfg_summary [other languages: ${ol_rows}]"
+ fi
+ olassert="$HERE/expected/$name.other-languages-assert"
+ if [ -f "$olassert" ]; then
+ olbad=$(python3 - "$olassert" "$w/actual.other-languages" <<'PYEOF'
+import sys
+have = set(l.rstrip('\n') for l in open(sys.argv[2]))
+n = 0
+for l in open(sys.argv[1]):
+ l = l.rstrip('\n')
+ if not l.strip() or l.startswith('#'): continue
+ n += 1
+ sign, row = l[:1], l[2:]
+ if sign == '+' and row not in have: print(' missing ' + row)
+ if sign == '-' and row in have: print(' present ' + row)
+if n == 0: print(' the assert file names no row')
+PYEOF
+)
+ if [ -n "$olbad" ]; then
+ echo "FAIL (other-languages assertion)"; echo "$olbad"; fail=$((fail+1)); failed+=("$name"); continue; fi
+ fi
+
# ── FIELD-ACCESS golden (#663) ────────────────────────────────────────────
# The edge golden says nothing about who reads or writes a field. This records every
diff --git a/graph/test/java/tools/normalize_other_languages.py b/graph/test/java/tools/normalize_other_languages.py
new file mode 100644
index 000000000..6edcc8537
--- /dev/null
+++ b/graph/test/java/tools/normalize_other_languages.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+"""The other-languages golden: every JavaScript and TypeScript file the parser took as source in a Java case, and
+every directory its JavaScript walk pruned.
+
+The Java goldens say nothing about the other analyzers the same parse runs, so a Maven project whose generated
+javadoc (target/site/apidocs/script.js) was extracted as a JavaScript project, and given a graph of its own, moved
+no Java golden at all (#1545). One line per module (`\tmodule\t`) and one per pruned directory
+(`javascript\tpruned\t\t`), repo-relative, sorted; nothing when the case has neither.
+
+usage: normalize_other_languages.py
+"""
+import csv, os, sys
+
+csv.field_size_limit(1 << 30)
+
+
+def rows(path):
+ if not os.path.isfile(path) or os.path.getsize(path) == 0: # a relation with no rows is a zero-byte file (#244)
+ return []
+ with open(path, newline='') as fh:
+ return list(csv.DictReader(fh, delimiter='\t'))
+
+
+def main():
+ if len(sys.argv) != 2:
+ sys.exit("usage: normalize_other_languages.py ")
+ ir = sys.argv[1]
+ out = []
+ for lang, name in (('javascript', 'all-javascript-modules.csv'), ('typescript', 'all-typescript-modules.csv')):
+ out += [f"{lang}\tmodule\t{r['filePath']}" for r in rows(os.path.join(ir, name))]
+ out += [f"javascript\tpruned\t{r['filePath']}\t{r['detail']}"
+ for r in rows(os.path.join(ir, 'skipped-javascript-files.csv')) if r.get('reason') == 'DIRECTORY_EXCLUDED']
+ if out:
+ print('\n'.join(sorted(out)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/parser/src/language-detectors/javascript-detector.ts b/parser/src/language-detectors/javascript-detector.ts
index f0c405b47..dacc4be0e 100644
--- a/parser/src/language-detectors/javascript-detector.ts
+++ b/parser/src/language-detectors/javascript-detector.ts
@@ -5,6 +5,7 @@ import { LanguageDetector } from './language-detector';
import { JS_SKIP_DIRECTORIES } from '@/constants/javascript-constants';
import { ProjectLanguage } from '@/types/ProjectInfo';
+import { isGeneratedOutputDirectory } from '@/utils/generated-output';
import { isJavaScriptSourceFile } from '@/utils/javascript';
/**
@@ -201,7 +202,8 @@ export class JavaScriptDetector implements LanguageDetector {
}
if (entry.isDirectory()
&& !entry.name.startsWith('.')
- && !JavaScriptDetector.SKIP.has(entry.name)) {
+ && !JavaScriptDetector.SKIP.has(entry.name)
+ && !isGeneratedOutputDirectory(dirPath, entry.name)) {
if (await this.hasJavaScriptSource(path.join(dirPath, entry.name), maxDepth - 1)) {
return true;
}
diff --git a/parser/src/language-detectors/typescript-detector.ts b/parser/src/language-detectors/typescript-detector.ts
index 7f64637bb..753d5d23f 100644
--- a/parser/src/language-detectors/typescript-detector.ts
+++ b/parser/src/language-detectors/typescript-detector.ts
@@ -5,6 +5,7 @@ import { LanguageDetector } from './language-detector';
import { TS_SKIP_DIRECTORIES, TS_SOURCE_EXTENSIONS } from '@/constants/typescript-constants';
import { ProjectLanguage } from '@/types/ProjectInfo';
+import { isGeneratedOutputDirectory } from '@/utils/generated-output';
/**
* Detects TypeScript projects.
@@ -89,7 +90,8 @@ export class TypeScriptDetector implements LanguageDetector {
}
if (entry.isDirectory()
&& !entry.name.startsWith('.')
- && !TypeScriptDetector.SKIP.has(entry.name)) {
+ && !TypeScriptDetector.SKIP.has(entry.name)
+ && !isGeneratedOutputDirectory(dirPath, entry.name)) {
if (await this.hasTypeScriptSource(path.join(dirPath, entry.name), maxDepth - 1)) {
return true;
}
diff --git a/parser/src/utils/generated-output.ts b/parser/src/utils/generated-output.ts
new file mode 100644
index 000000000..3f2176c2d
--- /dev/null
+++ b/parser/src/utils/generated-output.ts
@@ -0,0 +1,53 @@
+import * as fs from 'fs';
+import * as path from 'path';
+
+/**
+ * Is `parent/name` a directory a BUILD wrote, so its JavaScript and TypeScript are
+ * not the project's (#1545)?
+ *
+ * Recognised by what owns the directory, never by its name alone. A name is not
+ * evidence: `build`, `out` and `dist` are real Java package names, and pruning them
+ * by name is how the Java walk silently loses source (#531). So each rule here is
+ * anchored:
+ *
+ * `target` beside a `pom.xml` Maven's build directory. `mvn javadoc:javadoc`
+ * writes `target/site/apidocs/script.js`, and a
+ * Maven site or a frontend plugin writes more; a
+ * `target` with no `pom.xml` beside it is walked.
+ * a javadoc output directory `index.html` with `element-list` (JDK 9 and
+ * later) or `package-list` (JDK 8) beside it,
+ * wherever it is: a javadoc committed for a docs
+ * site (`docs/apidocs/`) is as generated as one
+ * under `target/`. The two list files are what
+ * javadoc writes for `-link` to read; no hand-kept
+ * JavaScript source names a file that way.
+ * a Dokka HTML output directory `index.html` with `navigation.html` and
+ * `scripts/sourceset_dependencies.js` beside it:
+ * the Kotlin documentation engine a Java library
+ * with Kotlin modules publishes beside its
+ * javadoc. Its per-module `package-list` sits one
+ * level down, so the javadoc rule misses it.
+ *
+ * Without this, one generated `script.js` made a Maven repository a JavaScript
+ * repository too: axiomcode-build counted it, built a second graph from javadoc's
+ * own helpers, and every query asked that graph as well.
+ * Used by the JavaScript and TypeScript walks and detectors, and by the project
+ * scan (project-scanner.ts), which must not register such a directory as a
+ * JavaScript root of its own: a walk never tests its own root. axiomcode-build's
+ * language count and the refresher's file table (ax_fresh.py `generated_output`)
+ * apply the same rules, so the language it detects is one the parser then finds
+ * files for.
+ */
+export function isGeneratedOutputDirectory(parent: string, name: string): boolean {
+ if (name === 'target' && fs.existsSync(path.join(parent, 'pom.xml'))) {
+ return true;
+ }
+ const directory = path.join(parent, name);
+ if (!fs.existsSync(path.join(directory, 'index.html'))) {
+ return false;
+ }
+ return fs.existsSync(path.join(directory, 'element-list'))
+ || fs.existsSync(path.join(directory, 'package-list'))
+ || (fs.existsSync(path.join(directory, 'navigation.html'))
+ && fs.existsSync(path.join(directory, 'scripts', 'sourceset_dependencies.js')));
+}
diff --git a/parser/src/utils/project-scanner.ts b/parser/src/utils/project-scanner.ts
index d7f8662ee..a0a9fa71e 100644
--- a/parser/src/utils/project-scanner.ts
+++ b/parser/src/utils/project-scanner.ts
@@ -5,6 +5,16 @@ import { EXCLUDED_DIRS, isJavaTestDir } from '@/constants/consts';
import { ProjectInfo, ProjectLanguage } from '@/types/ProjectInfo';
import { ProjectDetector } from '@/utils/project-detector';
import { isGitIgnoredDir } from '@/utils/git-ignored';
+import { isGeneratedOutputDirectory } from '@/utils/generated-output';
+
+/**
+ * The languages whose walks skip a build's output directory (#1545). The scan must
+ * skip it for them too: a javadoc committed at docs/apidocs/ with no package.json
+ * above it holds loose scripts, so without this the scan registers it as a
+ * JavaScript project ROOT, and a walk never tests its own root for being generated.
+ * Other languages still look inside: nothing about a javadoc says it holds no Python.
+ */
+const GENERATED_OUTPUT_LANGUAGES: readonly ProjectLanguage[] = [ProjectLanguage.JAVASCRIPT, ProjectLanguage.TYPESCRIPT];
export class ProjectScanner {
private detector: ProjectDetector;
@@ -94,7 +104,10 @@ export class ProjectScanner {
if (isGitIgnoredDir(subPath)) {
continue;
}
- await this.scanDirectory(subPath, projects, currentDepth + 1, maxDepth, claimedBelow, excludeTests);
+ const below = isGeneratedOutputDirectory(dirPath, entry.name)
+ ? new Set([...claimedBelow, ...GENERATED_OUTPUT_LANGUAGES])
+ : claimedBelow;
+ await this.scanDirectory(subPath, projects, currentDepth + 1, maxDepth, below, excludeTests);
}
} catch (error) {
console.error(`Error scanning directory ${dirPath}:`, error);
diff --git a/parser/src/workflows/javascript/javascript-project-analyzer.ts b/parser/src/workflows/javascript/javascript-project-analyzer.ts
index 02259361b..5ee34e3fe 100644
--- a/parser/src/workflows/javascript/javascript-project-analyzer.ts
+++ b/parser/src/workflows/javascript/javascript-project-analyzer.ts
@@ -27,6 +27,7 @@ import {
extractPackageEntries,
} from '@/parsers/javascript/package-entry-extractor';
import { EntityUtils } from '@/utils/entity-utils';
+import { isGeneratedOutputDirectory } from '@/utils/generated-output';
import { JsRelationWriter } from '@/workflows/javascript/js-relation-writer';
import { isJavaScriptSourceFile, stripJsExtension } from '@/utils/javascript';
import { JsBlockRegistry } from '@/analysis-types/javascript/JsBlockRegistry';
@@ -253,7 +254,7 @@ export class JavaScriptProjectAnalyzer {
const excludes = new Set(options.excludeDirs ?? JS_SKIP_DIRECTORIES);
// Counted, not merely skipped. See `collectJavaScriptFiles`.
const skippedByDirectory = new Map();
- const prunedDirectories: Array<{ directory: string; name: string; files: number }> = [];
+ const prunedDirectories: Array<{ directory: string; name: string; files: number; generated?: boolean }> = [];
const baseMservPath = options.baseMservPath === ''
? '' : realPathOf(path.resolve(options.baseMservPath));
const pathAnchor = pathAnchorFor(rootDir, baseMservPath);
@@ -382,7 +383,9 @@ export class JavaScriptProjectAnalyzer {
for (const pruned of prunedDirectories) {
this.recordSkip(pruned.directory, pathAnchor, options, serviceVersionLinkHash,
SkippedFileReason.DIRECTORY_EXCLUDED,
- `${pruned.files} JavaScript file(s) under an excluded directory named ${pruned.name}`);
+ pruned.generated === true
+ ? `${pruned.files} JavaScript file(s) under a build's output directory (${pruned.name})`
+ : `${pruned.files} JavaScript file(s) under an excluded directory named ${pruned.name}`);
}
await fsp.mkdir(options.outputDir, { recursive: true });
const writers = new Map();
@@ -697,7 +700,7 @@ function collectJavaScriptFiles(
* this answers "where", which is what a reader needs to tell a first-party
* package under `packages/node_modules` from an installed dependency.
*/
- prunedDirectories: Array<{ directory: string; name: string; files: number }>,
+ prunedDirectories: Array<{ directory: string; name: string; files: number; generated?: boolean }>,
/**
* Excluded names to walk anyway when they sit DIRECTLY under `rootDir` (#620):
* the build directory a package root's own `package.json` ships from.
@@ -737,7 +740,9 @@ function collectJavaScriptFiles(
if (isGitIgnoredDir(full) && !(directory === rootDir && walkUnderRoot.has(entry.name))) {
continue;
}
- if (!excludes.has(entry.name) || (directory === rootDir && walkUnderRoot.has(entry.name))) {
+ // a build's output directory is pruned like a skipped name, and counted like one (#1545)
+ const generated = isGeneratedOutputDirectory(directory, entry.name);
+ if (!generated && (!excludes.has(entry.name) || (directory === rootDir && walkUnderRoot.has(entry.name)))) {
walk(full);
continue;
}
@@ -745,7 +750,7 @@ function collectJavaScriptFiles(
if (cost > 0) {
skippedByDirectory.set(entry.name,
(skippedByDirectory.get(entry.name) ?? 0) + cost);
- prunedDirectories.push({ directory: full, name: entry.name, files: cost });
+ prunedDirectories.push({ directory: full, name: entry.name, files: cost, generated });
}
continue;
}
diff --git a/parser/src/workflows/typescript/typescript-project-analyzer.ts b/parser/src/workflows/typescript/typescript-project-analyzer.ts
index a4092ad32..e0bb97a35 100644
--- a/parser/src/workflows/typescript/typescript-project-analyzer.ts
+++ b/parser/src/workflows/typescript/typescript-project-analyzer.ts
@@ -28,6 +28,7 @@ import { moduleHashFor } from '@/parsers/typescript/extractors/ts-module-extract
import { TsConfigResolver } from '@/parsers/typescript/tsconfig-resolver';
import { TsRelationWriter } from './ts-relation-writer';
import { EntityUtils } from '@/utils/entity-utils';
+import { isGeneratedOutputDirectory } from '@/utils/generated-output';
import { stripTsExtension } from '@/parsers/typescript/ts-module-paths';
import { isGitIgnoredDir } from '@/utils/git-ignored';
@@ -722,7 +723,8 @@ function collectTypeScriptFiles(dir: string, excludes: ReadonlySet): str
for (const entry of entries) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
- if (!excludes.has(entry.name) && !entry.name.startsWith('.') && !isGitIgnoredDir(full)) {
+ if (!excludes.has(entry.name) && !entry.name.startsWith('.') && !isGitIgnoredDir(full)
+ && !isGeneratedOutputDirectory(current, entry.name)) {
walk(full);
}
continue;
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
index 8b375db2f..f6335793c 100644
--- a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
+++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
@@ -97,6 +97,21 @@ def prunes(lang, name, parent=''):
if lang == 'python': return name.endswith('.egg-info') or (parent == 'build' and bool(PY_BUILD_ARTIFACT.match(name)))
return False
+# A BUILD'S OUTPUT (#1545). The JavaScript and TypeScript parsers also skip a directory a build wrote, recognised by what
+# owns it and never by its name: Maven's `target` beside a pom.xml, a javadoc output directory (index.html with
+# element-list or package-list) and a Dokka one (index.html with navigation.html and scripts/sourceset_dependencies.js),
+# wherever they sit (parser/src/utils/generated-output.ts). Their SKIP sets above carry no `target`, as the parser's
+# lists carry none, so a directory merely named target stays watched (#531); this adds the anchored ones.
+WEB = frozenset({'javascript', 'typescript'})
+
+def generated_output(parent, name):
+ """True when `parent`/`name` is a directory a build wrote, whose JavaScript and TypeScript are not the project's"""
+ if name == 'target' and os.path.isfile(os.path.join(parent, 'pom.xml')): return True
+ d = os.path.join(parent, name)
+ if not os.path.isfile(os.path.join(d, 'index.html')): return False
+ 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')))
+
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')
@@ -146,6 +161,7 @@ def watched(root, lang):
spec = [(l, EXT.get(l, ()), NAMES.get(l, ())) for l in langs]
hidden = {root: frozenset()} # the languages that pruned each directory walked
ignored = git_ignored_dirs(root); real = os.path.realpath(root) # a .gitignore'd directory is pruned for every language
+ web = WEB.intersection(langs) # the languages that skip a build's output (#1545)
for d, subdirs, files in os.walk(root):
off = hidden.pop(d, frozenset())
rd = os.path.join(real, os.path.relpath(d, root)) if ignored else d
@@ -153,6 +169,7 @@ def watched(root, lang):
for s in subdirs:
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 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/multi_language.py b/tests/multi_language.py
index 7aa2a1a1b..46e059ac0 100644
--- a/tests/multi_language.py
+++ b/tests/multi_language.py
@@ -18,6 +18,8 @@
upgrade a graph built before this (one language, a file table without `lang_auto`) is stale in a repository
with other languages, so the first refresh indexes them
control a repository in one language gets no .axiomcode/lang and no fan-out
+ build output a Maven repository's target/ and a generated javadoc are not a JavaScript project (#1545); the same
+ repository with JavaScript of its own, one file in a directory named target, still gets that graph
python3 tests/multi_language.py [-v]
"""
@@ -39,6 +41,24 @@
'jslib/index.js': 'function helper(a) {\n return a + 1\n}\n\nfunction api(a) {\n return helper(a) * 2\n}\n\nmodule.exports = { api }\n',
}
+# a Maven project whose build wrote javadoc: target/ beside the pom.xml, and a copy committed for a docs site
+JAVADOC_JS = 'function loadScripts(doc, tag) {\n createElem(doc, tag, \'search.js\')\n}\n\nfunction createElem(doc, tag, path) {\n return doc.createElement(tag)\n}\n'
+MAVEN = {
+ 'pom.xml': '\n 4.0.0\n example\n widgets\n 1.0\n\n',
+ 'src/main/java/app/WidgetValidator.java': 'package app;\n\npublic class WidgetValidator {\n public boolean check(String name) {\n return name != null && !name.isEmpty();\n }\n}\n',
+ 'target/site/apidocs/script.js': JAVADOC_JS,
+ 'docs/apidocs/index.html': '\n\n',
+ 'docs/apidocs/element-list': 'app\n',
+ 'docs/apidocs/script.js': JAVADOC_JS,
+}
+# ...and its own JavaScript, one module of it in a directory named target that no pom.xml owns
+MAVEN_WEB = {
+ 'package.json': '{ "name": "widgets-web", "version": "1.0.0", "main": "src/main/js/app.js" }\n',
+ 'webpack.config.js': "module.exports = { entry: './src/main/js/app.js' }\n",
+ 'src/main/js/app.js': "const aim = require('./target/aim')\n\nfunction start(order) {\n return aim.point(order)\n}\n\nmodule.exports = { start }\n",
+ 'src/main/js/target/aim.js': 'function point(order) {\n return order\n}\n\nmodule.exports = { point }\n',
+}
+
def sh(cwd, *cmd, env=None):
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env=env)
@@ -167,6 +187,28 @@ def check(ok, why, detail=''):
check(tab.get('lang') == 'typescript', 'control: its file table names the one language', json.dumps({k: tab.get(k) for k in ('lang', 'lang_auto')}))
st = json.loads(sh(one, sys.executable, FRESH, 'status', '.', '--json', env=quiet).stdout or '{}')
check(st.get('state') == 'fresh', 'control: and it is fresh', json.dumps(st))
+
+ # ── build output ──────────────────────────────────────────────────────────────────────────────────────
+ mvn = os.path.join(work, 'maven'); make(mvn, MAVEN)
+ b = sh(mvn, AX, 'index', '.', env=quiet)
+ check(b.returncode == 0 and 'building java graph for' in b.stdout and 'javascript 0 ' in b.stdout
+ and not os.path.exists(os.path.join(mvn, '.axiomcode', 'lang')),
+ 'build output: javadoc under target/ and a committed javadoc do not make a Maven repository a JavaScript one', b.stdout + b.stderr)
+ n = sh(mvn, AX, 'impact', 'createElem', '.', env=quiet)
+ check(n.returncode != 0 and 'nothing named' in n.stdout, 'build output: and no graph answers for javadoc\'s own functions', n.stdout + n.stderr)
+ st = json.loads(sh(mvn, sys.executable, FRESH, 'status', '.', '--json', env=quiet).stdout or '{}')
+ check(st.get('state') == 'fresh', 'build output: and the graph is fresh, not waiting on a language it will never build', json.dumps(st))
+ web = os.path.join(work, 'maven-web'); make(web, dict(MAVEN, **MAVEN_WEB))
+ b = sh(web, AX, 'index', '.', env=quiet)
+ # three JavaScript files to one Java file, so JavaScript is the main language and Java the other one
+ check(b.returncode == 0 and 'javascript + java' in b.stdout and 'javascript 3 ' in b.stdout
+ and os.path.exists(os.path.join(web, '.axiomcode', 'lang', 'java', 'out', 'graph.sqlite')),
+ 'build output (control): the same repository with JavaScript of its own gets its graph, all three files', b.stdout + b.stderr)
+ j = sh(web, AX, 'path', 'start', 'point', '.', env=quiet)
+ check(j.returncode == 0 and 'verified' in j.stdout and 'src/main/js/target/aim.js' in j.stdout,
+ 'build output (control): a directory named target that no pom.xml owns is source', j.stdout + j.stderr)
+ n = sh(web, AX, 'impact', 'createElem', '.', env=quiet)
+ check(n.returncode != 0 and 'nothing named' in n.stdout, 'build output (control): and javadoc\'s functions are still not in it', n.stdout + n.stderr)
finally:
shutil.rmtree(work, ignore_errors=True)
print(f"\n{'FAILED: ' + str(len(fails)) if fails else 'all passed'}")