tool: generate configuration properties reference by cstamas · Pull Request #2145 · eclipse-openvsx/openvsx · GitHub
Skip to content

tool: generate configuration properties reference - #2145

Merged
netomi merged 6 commits into
eclipse-openvsx:mainfrom
cstamas:config-doco
Sep 8, 2026
Merged

netomi merged 6 commits into
eclipse-openvsx:mainfrom
cstamas:config-doco

Conversation

@cstamas

@cstamas cstamas commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Adds a JBang script that scans src/main/java for @Value("${...}") and @ConfigurationProperties(...) usages and generates a Markdown reference of every configuration property the server reads — key, default value, source location, and any @ConfigurationProperties prefix with its bound fields.

There's no spring-boot-configuration-processor wired into this project, and even if there would be, it generates incomplete list, so there's currently no generated list of config properties to reference; this fills that gap without adding a build-time dependency.

  • server/scripts/src/ConfigPropertiesReport.java — Java 25 JBang script (pure JDK, no external dependencies), following the existing convention in scripts/src/ (see DependencyOverrideCheck.java as example)
  • server/scripts/config-properties-report.sh — thin wrapper, mirrors dependency-override-check.sh

Regex-based source scanning (no AST parser) that correctly handles the real edge cases present in this codebase: nested ${outer:${inner:default}} defaults, SpEL-wrapped @Value expressions, string-concatenated @Value args resolved against same-file constants, @ConfigurationProperties prefixes declared via a constant reference (RateLimitProperties.PROPERTY_PREFIX), and @ConfigurationProperties on @Bean-returned collections regardless of annotation order (MirrorConfig).

Out of scope: cross-referencing application*.yml/.properties for actually-configured values, and recursing into @ConfigurationProperties nested field types beyond one level (e.g. RemoteScannerProperties's Map<String, ScannerConfig> keys are operator-chosen at runtime and aren't enumerable from source).

Example output:
https://gist.github.com/cstamas/f853eec0307f8d5977106bf69f505d29


A bit unrelated: tidy up the buildSrc sub-project to be Java 25 (as "outer build") as it now aligns the two, and IDEs like Idea are not confused about it anymore. Also JBang IDEA plugin now works just fine.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new ConfigPropertiesReport.java has confirmed correctness/compilation issues (record component access + text-block handling + merging behavior) that will produce a broken or inaccurate report.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new server-side developer script to generate a Markdown reference of Spring configuration properties by scanning @Value("${...}") and @ConfigurationProperties(...) usages, and aligns the server scripts/build tooling with Java 25.

Changes:

  • Add ConfigPropertiesReport.java (JBang) plus a config-properties-report.sh wrapper to generate a configuration properties reference from source.
  • Update existing JBang scripts to declare //JAVA 25+.
  • Bump maven-artifact used by the dependency override checker and align buildSrc compilation to Java 25.
File summaries
File Description
server/scripts/src/ImportSort.java Declares Java 25+ for the JBang script.
server/scripts/src/DependencyOverrideCheck.java Updates Maven artifact dependency and declares Java 25+.
server/scripts/src/ConfigPropertiesReport.java New JBang script that scans sources and prints a Markdown config-properties report.
server/scripts/src/ClosingBraceFix.java Declares Java 25+ for the JBang script.
server/scripts/src/AddBracesFix.java Declares Java 25+ for the JBang script.
server/scripts/config-properties-report.sh New wrapper to run the report script from the server root.
server/buildSrc/build.gradle Updates Maven artifact version and sets Java compatibility to 25.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread server/scripts/src/ConfigPropertiesReport.java
Comment thread server/scripts/src/ConfigPropertiesReport.java
Comment thread server/scripts/src/ConfigPropertiesReport.java
Comment thread server/scripts/src/ConfigPropertiesReport.java Outdated
@cstamas

cstamas commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

@cstamas
cstamas marked this pull request as ready for review September 7, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new tooling introduces correctness/build risks (Gradle Java compatibility configuration and regex constant-resolution ambiguity) that should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

server/scripts/src/ConfigPropertiesReport.java:214

  • Same issue as in scanValueAnnotations: this regex can match the constant name as a substring of another identifier and resolve the wrong @ConfigurationProperties prefix. Use identifier boundaries to ensure only the intended constant is matched.
            var name = expr.contains(".") ? expr.substring(expr.lastIndexOf('.') + 1) : expr;
            var constantValueMatcher =
                    Pattern.compile(Pattern.quote(name) + "\\s*=\\s*\"([^\"]*)\"").matcher(raw);
            if (constantValueMatcher.find()) {
                prefix = constantValueMatcher.group(1);
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread server/buildSrc/build.gradle
Comment thread server/scripts/src/ConfigPropertiesReport.java

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new ConfigPropertiesReport.java currently won’t compile due to record component field access and should also align its main signature with the repo’s JBang script convention.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

server/scripts/src/ConfigPropertiesReport.java:39

  • All other JBang scripts in server/scripts/src define void main(String[] args), but this script defines void main() while the wrapper forwards "$@". To avoid surprising invocation behavior and stay consistent with the existing scripts, accept String[] args (even if unused).

server/scripts/src/ConfigPropertiesReport.java:70

  • PropertyEntry is a record, so its components are not accessible as fields from here; using entry.defaultValue, entry.sources, entry.note, and entry.key will not compile. Use the record accessor methods instead (defaultValue(), sources(), note(), key()).
        var defaultCell = entry.defaultValue == null || entry.defaultValue.isEmpty()
                ? "-"
                : "`" + mdEscape(entry.defaultValue) + "`";
        var sourceCell =
                String.join(", ", entry.sources.stream().map(s -> "`" + s + "`").toList());
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@cstamas cstamas mentioned this pull request Sep 8, 2026
cstamas and others added 4 commits September 8, 2026 11:08
Adds a jbang script that scans src/main/java for @value("${...}") and @ConfigurationProperties(...) usages and generates a Markdown reference of every configuration property the server reads — key, default value, source location, and any @ConfigurationProperties prefix with its bound fields.

There's no spring-boot-configuration-processor wired into this project, so there's currently no generated list of config properties to reference; this fills that gap without adding a build-time dependency.

- server/scripts/src/ConfigPropertiesReport.java — jbang script (pure JDK, no external dependencies), following the existing convention in scripts/src/ (see DependencyOverrideCheck.java)
- server/scripts/config-properties-report.sh — thin wrapper, mirrors dependency-override-check.sh

Regex-based source scanning (no AST parser) that correctly handles the real edge cases present in this codebase: nested ${outer:${inner:default}} defaults, SpEL-wrapped @value expressions, string-concatenated @value args resolved against same-file constants, @ConfigurationProperties prefixes declared via a constant reference (RateLimitProperties.PROPERTY_PREFIX), and @ConfigurationProperties on @Bean-returned collections regardless of annotation order (MirrorConfig).

Out of scope: cross-referencing application*.yml/.properties for actually-configured values, and recursing into @ConfigurationProperties nested field types beyond one level (e.g. RemoteScannerProperties's Map<String, ScannerConfig> keys are operator-chosen at runtime and aren't enumerable from source).

Example output:
https://gist.github.com/cstamas/f853eec0307f8d5977106bf69f505d29
As right now IDEA goes lala
- addEntry() now merges deterministically: whichever occurrence of a
  duplicate key provides a non-empty default/note wins, instead of the
  first-scanned occurrence silently shadowing a later one's data
- blank text blocks before brace/paren scanning, so braces, parens, and
  semicolons inside one (e.g. SQL in an @query) can't throw off the
  structural scans
- record each property's source relative to src/main/java instead of
  the bare filename, since it can be ambiguous across packages

Confirmed unaffected: PropertyEntry/ConfigClassEntry record field access
compiles fine here despite being flagged - this is a JEP 445 single-file
source script, so the records are nested types of the implicit enclosing
class and private field access between them is legal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@netomi netomi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current main and reviewed. The tool works and the parsing is more careful than I
expected for a regex approach — the tricky cases in the PR description really do come out right
(nested ${outer:${inner:default}}, the SpEL-wrapped @Value, RateLimitProperties.PROPERTY_PREFIX,
MirrorConfig's annotation order). My findings are about completeness of the output rather than
correctness of the parsing.

Rebase

main had moved to 08ad497a3 (#2176), which already added //JAVA 25+ to the four scripts this PR
also touched — that was the conflict. Resolved as:

  • DependencyOverrideCheck.java — kept your maven-artifact bump to 3.9.16, with main's
    //JAVA-before-//DEPS ordering.
  • ImportSort.java — kept main's spotless-lib:4.10.2. Your side had 3.3.1, which predates #2176
    moving it; restoring it would both undo that fix and fail the new formatter-version-check.sh.

The //JAVA 25+ churn on AddBracesFix/ClosingBraceFix/ImportSort is now redundant and dropped
out, so the diff went from 7 files to 4. MERGEABLE again. Force-pushed with --force-with-lease,
so nothing of yours could have been clobbered.

Verified on the rebased tree: spotlessCheck, formatter-version-check.sh, :buildSrc:compileJava,
and dependency-override-check.sh (confirming maven-artifact 3.9.16 works for its one consumer) all
pass, and the report itself runs and produces 206 keys.

Copilot's findings

Four of the six are already fixed on the branch: blankTextBlocks exists and runs first, source
locations are root.relativize(path), addEntry merges defaults/notes explicitly, and buildSrc uses
JavaVersion.VERSION_25.

One is simply wrong — the claim that field-style access on PropertyEntry "will not compile".
PropertyEntry is declared inside the implicit class, so its private components are accessible from
the enclosing class body. It compiles and runs. Worth not "fixing" on Copilot's say-so; the only real
point there is that the file mixes entry.key with entry.note(), which is a style inconsistency.

One is still open and correct: the constant lookup at both call sites has no identifier
boundaries, so Pattern.quote(name) + "\\s*=\\s*\"...\"" matches FOO inside MY_FOO. I checked
whether it currently misfires — adding \b on both sides produces byte-identical output, so it is
latent rather than active. Cheap to fix while it's cheap.

Findings of my own

1. @ConditionalOnProperty isn't scanned, and four real properties are missing as a result.
This is the one I'd want fixed. These keys are read only from @ConditionalOnProperty — no @Value
anywhere — so they appear nowhere in the reference:

Property Declared in
ovsx.redis.enabled cache/CacheConfig.java
ovsx.storage.migration.enabled storage/StorageMigration.java
ovsx.upstream.proxy.enabled UpstreamProxyService.java
spring.web.error.path web/ServerErrorController.java

The first three are operator-facing switches that gate whole subsystems, which makes them among the
most important things such a document should list. @ConditionalOnProperty also supports
prefix = <constant> (RateLimitConfig uses RateLimitProperties.PROPERTY_PREFIX), so the
constant-resolution machinery already in the script is what's needed. The PR description lists what's
out of scope and this isn't in it, so I assume it was an oversight rather than a decision.

2. Keys that exist only as a nested fallback default are dropped. AccessTokenConfig has:

@Value("#{'${ovsx.access-token.prefix:${ovsx.token-prefix:}}'}")

The report lists ovsx.access-token.prefix with default `${ovsx.token-prefix:}` but never lists
ovsx.token-prefix itself. That's the deprecated-key-fallback pattern AGENTS.md mandates for any
property that has shipped, so it's a recurring shape, and it's precisely the key an operator with the
old configuration needs to look up. PLACEHOLDER already captures the nested group; only the outer
match is used. (The other nested case, ovsx.trusted-publishing.audience:${ovsx.webui.url:}, is
covered only by luck — ovsx.webui.url has its own @Value elsewhere.)

3. @ConfigurationProperties field defaults are parsed and then thrown away. Around 30 of the 206
keys show - in the Default column while the source has a plain literal initializer. ovsx.rate-limit
is all eight of its keys:

private Boolean enabled = false;
private String tokenPrefix = "";
private String ipAddressFunction = "getRemoteAddr()";
private String defaultHttpContentType = "application/json";

addField computes declaration.split("=", 2)[0] and discards the right-hand side. Passing a simple
literal through (and leaving expressions like List.of("svg") or ExtensionProcessor.DEFAULT_MAX_TAGS
as -, or resolving the latter with the constant lookup that already exists) would fill in the column
that makes the document useful. Not a bug, but the biggest cheap win available.

4. trimmed.contains("static") is a substring test. Same class of issue as the word boundaries: a
field like String staticPath would be silently skipped as though it were a static member. No such
field exists today, so latent — but it fails by omitting a property, which is the direction that
doesn't get noticed.

5. Minor: every field of a @ConfigurationProperties class reports the annotation's line rather
than the field's, so all five ovsx.publishing.* keys point at PublishingConfig.java:24. @Value
entries are line-accurate, so the inconsistency stands out. Fine if deliberate.

Also worth noting

maven-artifact is now a third "must match" pair (buildSrc/build.gradle and this script's //DEPS)
that nothing enforces — formatter-version-check.sh covers only jdt.core and spotless-lib, since
maven-artifact isn't a formatter dependency. Not for this PR, but the check could grow a generic
"these declarations must agree" mode.

Happy to push fixes for 1–4 to this branch if you'd rather not round-trip.

@cstamas

cstamas commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

@netomi go for it, thanks!

Addresses the review findings, with cstamas's go-ahead.

@ConditionalOnProperty is now scanned. Four properties were read only from that
annotation and so appeared in no reference at all - ovsx.redis.enabled,
ovsx.storage.migration.enabled, ovsx.upstream.proxy.enabled and
spring.web.error.path - three of which are operator-facing switches gating whole
subsystems. Only prefix/name/value are keys; havingValue and matchIfMissing are
not, so the attributes are read by name rather than by taking every string
literal in the annotation. A prefix given as a constant from another class
(RateLimitConfig) cannot be resolved from the file being scanned, and is
reported on stderr and skipped rather than emitted under a made-up key - that
one key is documented anyway from the properties class it belongs to.

A key that exists only as a nested fallback default was dropped: `${a:${b:}}`
declares b as well, which is the pattern AGENTS.md requires of any key that has
shipped, so ovsx.token-prefix - the old name of ovsx.access-token.prefix - is
exactly what an operator with old configuration comes here to look up.

@ConfigurationProperties field initializers were parsed and then discarded.
Plain literals are now reported, which fills in four of the empty Default cells;
expressions (List.of("svg"), a constant from another class) are still left blank
rather than printed as source an operator cannot use. Read from the raw text
rather than the blanked copy, which had replaced every string literal with x's -
the blanking passes all preserve length, so the two are index-aligned.

Both constant lookups were missing identifier boundaries, so FOO could resolve
against MY_FOO, and the class-body scan tested contains("static"), which would
skip a field named staticPath as though it were a static member. Both were
latent - the output is unchanged by either fix - and both fail by omitting a
property, which is the direction nobody notices.

Scanning the conditionals pushed one key's source list to two dozen entries, so
the cell now names three and counts the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Took you up on the go-ahead — findings 1–4 are in 1d9c590, and I have replied to and resolved the
six Copilot threads. Four of those were already fixed by your earlier commits and had simply never
been marked resolved; one was wrong (the record-access claim); one was real and is fixed now.

@ConditionalOnProperty is scanned. Four properties were read only from that annotation, so
they appeared in no reference at all:

Property Declared in
ovsx.redis.enabled cache/CacheConfig.java
ovsx.storage.migration.enabled storage/StorageMigration.java
ovsx.upstream.proxy.enabled UpstreamProxyService.java
spring.web.error.path web/ServerErrorController.java

Only prefix/name/value are keys, so the attributes are read by name rather than by taking every
string literal — otherwise havingValue = "/server-error" becomes a property. Verified that no
havingValue leaked in.

One case cannot be resolved: RateLimitConfig gives its prefix as
RateLimitProperties.PROPERTY_PREFIX, a constant in another file. Rather than emit a made-up key it
warns on stderr and skips:

warning: org/eclipse/openvsx/ratelimit/config/RateLimitConfig.java has
@ConditionalOnProperty(prefix = RateLimitProperties.PROPERTY_PREFIX), which is not
resolvable in this file; its keys are omitted

ovsx.rate-limit.enabled is documented anyway from the properties class it belongs to, so nothing is
actually lost — but it says so out loud rather than quietly dropping it, since quietly omitting a
property is the failure this script exists to prevent.

Nested fallback keys. ${a:${b:}} declares b too. ovsx.token-prefix now appears with the note
fallback for ovsx.access-token.prefix — which is the deprecated-key pattern AGENTS.md requires of
anything that has shipped, and precisely the key an operator with old configuration comes here to look
up.

Field defaults. addField was splitting the initializer off and dropping it. Plain literals are
now reported:

| `ovsx.rate-limit.default-http-content-type` | `application/json` |
| `ovsx.rate-limit.default-http-response-body` | `{ "message": "Too many requests!" }` |
| `ovsx.rate-limit.enabled`                   | `false` |
| `ovsx.rate-limit.ip-address-function`       | `getRemoteAddr()` |

I should correct my own review here: I said "around 30 keys" lose a recoverable default, and that was
too strong. Only four are plain literals. The rest are expressions (List.of("svg"),
Duration.ofMinutes(5)) or constants from other classes (ExtensionProcessor.DEFAULT_MAX_TAGS), and
those stay blank rather than printing source an operator cannot put in a yaml file.

One subtlety worth knowing: the class-body scan works on the blanked copy, where string literals
have been replaced by xs — reading the initializer from it produced
`xxxxxxxxxxxxxxxx` as the default. It now reads the raw text at the same offsets, which is safe
because every blanking pass preserves length.

Both boundary fixes (the constant lookup, and contains("static") in the class-body scan) are in.
Both were latent — the generated report is byte-identical either way — but both fail by omitting a
property.

One thing I caused and then fixed: scanning the conditionals pushed ovsx.data.mirror.enabled to
two dozen source locations, which made that row unreadable. The cell now names three and counts the
rest.

Net: 200 → 205 property keys, nothing removed. Report runs clean.

Still open from my review, and deliberately not touched: @ConfigurationProperties fields report the
annotation's line rather than the field's, so all five ovsx.publishing.* keys point at
PublishingConfig.java:24. Happy to fix if you want it, but it is cosmetic and I did not want to keep
growing the diff.

This was the only script in scripts/src with a no-argument `void main()`; the
other five take `String[] args`. jbang deduces the main class, and a version
that does not recognise a no-argument instance main fails the script with "no
main class deduced, specified nor found in a manifest" rather than anything
that points at the cause.

Not reproducible on jbang 0.141.0, where both forms run, so this is the
portable form rather than a verified fix for one version. It costs nothing and
removes the only structural difference from the scripts that do work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi
netomi merged commit a01971e into eclipse-openvsx:main Sep 8, 2026
5 checks passed
@cstamas
cstamas deleted the config-doco branch September 9, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants