Skip to content

feat(ci): enforce lowerCamelCase and max depth in reference.conf#6795

Open
bladehan1 wants to merge 3 commits into
tronprotocol:release_v4.8.2from
bladehan1:ci/reference-conf-gate
Open

feat(ci): enforce lowerCamelCase and max depth in reference.conf#6795
bladehan1 wants to merge 3 commits into
tronprotocol:release_v4.8.2from
bladehan1:ci/reference-conf-gate

Conversation

@bladehan1
Copy link
Copy Markdown
Collaborator

@bladehan1 bladehan1 commented May 21, 2026

What does this PR do?

Adds a CI gate that validates common/src/main/resources/reference.conf on every PR / push:

  • Every dot-separated segment of every key must match ^[a-z][a-zA-Z0-9]*$: starts with a lowercase ASCII letter, then ASCII letters/digits only. Acronym runs after position 1 (e.g. httpPBFTEnable, openHistoryQueryWhenLiteFN, allowShieldedTRC20Transaction) are accepted — only the first character is constrained, which matches what java.beans.Introspector / ConfigBeanFactory actually require for bean-property auto-binding.
  • Total path depth must be ≤ 5 (set exactly at the current max in tree — no buffer; new deeper keys must explicitly bump MAX_DEPTH via a reviewed change).
  • Seven legacy keys are grandfathered via an explicit ALLOWLIST (4 PBFT + 3 node.shutdown.*).
  • Array steps count toward depth (each [] = +1 level); nested arrays (HOCON list-of-list) are supported.

Implementation: .github/scripts/check_reference_conf.py using pyhocon (pinned via .github/scripts/requirements.txt), wired into the existing checkstyle job in .github/workflows/pr-check.yml. actions/setup-python enables pip caching keyed on the requirements file. Violations produce per-line stdout output plus one consolidated ::error file=...,title=reference.conf::... GHA annotation so failures show up in the PR check summary.

Why are these changes required?

reference.conf is the single source of default values for every config key in java-tron, and key names must auto-bind to XxxConfig.java bean fields via Typesafe Config's ConfigBeanFactory. This gate is stricter than typical Java/Spring projects (Spring Boot's Binder / Quarkus / Micronaut tolerate kebab-case, snake_case, UPPER_CASE, and camelCase as equivalent forms) because ConfigBeanFactory uses java.beans.Introspector and requires the HOCON key to match the JavaBean property name exactly — no case normalization. Without a CI gate, non-conforming keys silently fail to bind and accumulate.

A depth ceiling also prevents deeply nested hierarchies from creeping in. The gate is set exactly at the current max with no buffer: silent drift is unacceptable for a mature project, so any new key that needs to go deeper has to bump MAX_DEPTH explicitly and be reviewed.

This PR is the release_v4.8.2 counterpart of #6792 (which targets develop), so the same gate runs on hotfix PRs against the release branch.

This PR has been tested by:

  • Manual Testing: ran the script against release_v4.8.2's reference.conf — passes with all keys, max depth 5.
  • Local fixture suite (not committed) covering: empty / simple / dotted-form / at-max-depth / object array / scalar array / allowlist / format violations (Pascal / snake / digit / array-internal / non-allowlisted acronym) / depth violations (dotted + in-array) / combined + dedup invariant / env errors (parse failure, missing file). All assertions PASS.
  • Verified: parse failure (invalid HOCON) and missing file both exit 2 with ::error annotation; a single user-declared deep key produces exactly one depth violation line (leaf-only filtering).

Follow up

Extra details

  • Allowlist exempts 7 legacy keys to keep user config.conf compatible: node.http.PBFTEnable, node.http.PBFTPort, node.rpc.PBFTEnable, node.rpc.PBFTPort, plus node.shutdown.BlockTime/BlockHeight/BlockCount (PascalCase exceptions handled manually in NodeConfig.fromConfig; currently commented out in reference.conf — pre-listed so the gate stays green if ever uncommented with defaults).
  • Library choice: pyhocon over Typesafe Config because the gate runs before Gradle/JDK setup in CI (~3 steps: setup-python + pip install + invoke). Both implement the HOCON spec; outputs are equivalent.

bladehan1 added 2 commits May 21, 2026 10:51
Add a CI gate that scans common/src/main/resources/reference.conf and
fails the build when any key violates lowerCamelCase
(^[a-z][a-zA-Z0-9]*$ per dot-separated segment) or exceeds the maximum
hierarchy depth (5). Array element keys are validated the same way;
each array step counts as one depth level — e.g. an inner field at
`rate.limiter.rpc[].component` is depth 5.

Parsing is delegated to pyhocon, the reference Python HOCON
implementation. It returns a fully-merged ConfigTree where dotted-form
keys expand into nested objects — the same canonical key set Typesafe
Config and ConfigBeanFactory see at runtime — and handles
triple-strings, substitutions, includes, +=, and block comments
without us re-implementing the grammar.

Four legacy PBFT* keys are grandfathered via an in-script allowlist
so the gate fails only on new violations. A consolidated GHA error
annotation lists every offending key, and sys.exit(1) drives step
failure. The script also accepts `--debug` to print every parsed key
with its depth (trailing `/` marks namespace intermediates) for
manual verification against the source file.

Runs as a new step in the existing checkstyle job of pr-check.yml
(setup-python + `pip install pyhocon`), so no extra runner spin-up.
Pin pyhocon via .github/scripts/requirements.txt so the gate is not
exposed to silent behavior changes in upstream releases, and enable
actions/setup-python's pip cache (keyed off the requirements file)
so subsequent CI runs skip the PyPI round-trip.
@barbatos2011
Copy link
Copy Markdown
Contributor

Solid CI gate, with two small items to address in this PR plus a recommendation on how to land the broader gate strategy.

Two issues to address in this PR

1. node.shutdown.BlockTime / BlockHeight / BlockCount should be in ALLOWLIST

PR #6790's docs/configuration-conventions.md explicitly lists these as a third documented PascalCase exception (alongside the PBFT* family and isOpenFullTcpDisconnect), handled via manual reads in NodeConfig.fromConfig. They pass the gate today only because they're commented out in reference.conf. If anyone ever uncomments them with default values (e.g. a future "graceful shutdown defaults" PR), CI rejects a legitimate documented exception.

Suggested addition:

ALLOWLIST = {
    "node.http.PBFTEnable",
    "node.http.PBFTPort",
    "node.rpc.PBFTEnable",
    "node.rpc.PBFTPort",
    # node.shutdown.BlockTime/BlockHeight/BlockCount: PascalCase exceptions
    # documented in configuration-conventions.md, read manually in
    # NodeConfig.fromConfig. Currently commented out in reference.conf;
    # listed here to prevent CI breakage if ever uncommented with defaults.
    "node.shutdown.BlockTime",
    "node.shutdown.BlockHeight",
    "node.shutdown.BlockCount",
}

2. Regex description vs actual behavior

The regex ^[a-z][a-zA-Z0-9]*$ is described as enforcing "lowerCamelCase," but it actually accepts acronym runs anywhere after position 1 — e.g. httpPBFTEnable, pBFTExpireNum, openHistoryQueryWhenLiteFN, allowShieldedTRC20Transaction all pass (and all exist in current reference.conf).

This is a conscious choice — those names work with ConfigBeanFactory because they start lowercase, which is the actual constraint — but the script docstring / PR description should be precise so reviewers don't later challenge them as violations. Suggested wording:

"Each segment must start with a lowercase ASCII letter and contain only ASCII letters/digits. Acronym runs after position 1 (e.g. httpPBFTEnable) are accepted: this matches what java.beans.Introspector tolerates, which is what ConfigBeanFactory actually needs."

Context: why a strict gate is justified

The lowerCamelCase + depth-limit approach is stricter than typical Java/Spring projects, which usually rely on bean binding to catch silent failures plus relaxed name matching (Spring Boot's Binder / ConfigurationPropertyName API tolerates kebab-case, snake_case, UPPER_CASE, and camelCase as equivalent forms; Quarkus and Micronaut behave similarly). The aggressive gate here is justified because java-tron uses Typesafe Config's ConfigBeanFactory, which uses java.beans.Introspector and requires the HOCON key to match the JavaBean property name exactly — no case normalization. Without a CI gate, non-conforming keys silently fail to bind and accumulate.

One sentence in the PR description acknowledging "stricter than typical Java projects, but necessary because ConfigBeanFactory does no case normalization" would help future contributors understand the choice.

Recommendation: extend this PR with Gates 2–5 as separate commits

Think of this PR as Gate 1. A new config key being "reasonable" actually has 5–6 independent dimensions. Suggest landing all of them in this PR with one commit per gate (rather than as follow-up PRs):

Gate Goal Catches Implementation
1 (this commit) Naming + depth Foo, some_key, depth > 5 Python + pyhocon
2 Bean ↔ key parity + default drift key has no bean field, field has no key, Java default ≠ reference.conf default Python + javalang or Gradle JUnit using Class.getDeclaredFields() + ConfigFactory.defaultReference()
3 Binding chain completeness bean field exists but applyXxxConfig doesn't assign to PARAMETER, or no production consumer reads getXxx() Java AST or Gradle JUnit reflection
4 Inline comment coverage Leaf keys without #/// documentation Python + pyhocon + raw text scan (carry-forward inline comments)
5 XxxConfigTest coverage Bean field present but no test assertion references it Gradle JUnit (reflection on test bytecode)

(Gate 6 — deprecation policy via git history scan — is qualitatively different and lower priority; defer.)

Why one PR + multi-commit rather than 5 follow-up PRs:

  1. Shared infrastructure — Gates 1/2/4 all use pyhocon, share GHA annotation emission, live in .github/scripts/. Keeping them together avoids cross-PR refactoring.
  2. Coherent design surface — reviewer can spot rules that interact (e.g. comment-coverage gate's line-attribution must match how naming gate walks segments).
  3. Avoids the "follow-up never happens" trap — multi-PR roadmaps in OSS projects often stall after Gate 1 lands.
  4. Independent rollback preservedgit revert <gate-N-commit-sha> is as clean as reverting a standalone PR.
  5. Project velocity context — the config series (refactor(config): simplify applyEventConfig (follow-up to #6615) #6735, fix(config): optimizes config binding for node  #6755, refactor(config): merge test config files #6788, docs(config): add configuration guides #6790, refactor(config): remove unused storage index and json parsing config #6794, this PR) is concentrated in a few contributors; one focused PR avoids ×5 scoping/CI/review overhead.

Why Gate 2 is the highest-leverage follow-up: it's exactly the gate that would have caught the recent class of issues this series has been fixing manually:

Suggested commit layout:

1. feat(ci): add reference.conf naming + depth gate           ← current PR content
2. feat(ci): add bean ↔ key parity + default drift gate       ← Gate 2 (highest ROI)
3. feat(ci): add config binding chain completeness gate
4. feat(ci): add inline comment coverage gate
5. feat(ci): add XxxConfigTest field coverage gate

For Gate 2 implementation, Gradle JUnit using Java reflection is likely simpler than Python + AST parsingClass.getDeclaredFields() walks bean fields naturally, no Java AST parser needed. ~4–6 hr starting cost.

TL;DR

  • ✅ Approve in principle — Gate 1 is solid, regex/depth logic verified empirically (296 keys, max depth 5)
  • 🟠 Fix 2 small items before merge — add shutdown.BlockTime/Height/Count to ALLOWLIST; tighten regex description in script
  • 📋 Suggest extending to all 5 gates in this PR as separate commits, rather than 5 follow-up PRs — shared infrastructure, coherent design surface, avoids the "follow-up never happens" trap

Add three PascalCase node.shutdown.{BlockTime,BlockHeight,BlockCount}
entries to ALLOWLIST. They are documented exceptions handled manually
in NodeConfig.fromConfig (not via ConfigBeanFactory) and are currently
commented out in reference.conf, so the gate does not see them today;
pre-listing keeps CI green if a future change uncomments them with
defaults.

Tighten the docstring for KEY_REGEX: the rule accepts acronyms from
position 1 onward (e.g. httpPBFTEnable, openHistoryQueryWhenLiteFN,
allowShieldedTRC20Transaction). That matches what
java.beans.Introspector / ConfigBeanFactory actually require — only
the first character must be lowercase — and avoids future reviewers
mistaking already-conformant keys for violations.
@bladehan1
Copy link
Copy Markdown
Collaborator Author

Two changes have been implemented.
Agreed that Gate 2 is the highest ROI going forward, but this PR should not be merged. The scope of Gate 2 (which packages count as config beans, cross-type default value comparison, Args.java boundary, toolchain selection) is substantial enough for its own PR.

Gates 3/4/5 overlap with Gate 2.Gate 2's scope must be clarified first before we can determine whether 3/4/5 still stand independently

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants