Skip to content

B2: redeclaration checked per scope, per namespace - #16

Merged
milyin merged 1 commit into
docs/validation-umbrellafrom
step/b2-scopes-and-namespaces
Aug 6, 2026
Merged

B2: redeclaration checked per scope, per namespace#16
milyin merged 1 commit into
docs/validation-umbrellafrom
step/b2-scopes-and-namespaces

Conversation

@milyin

@milyin milyin commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Step B2 of #6. Stacked on #15. The core of the umbrella.

The duplicate check had three problems and one false positive.

It was looking where the problem isn't

It only ever examined top-level declarations. In the downstream consumer's
real output, functions overwhelmingly live inside classes and objects:

file functions top-level
session.kt 43 0
keyexpr.kt 24 0
pubsub.kt 44 4

It skipped two whole declaration kinds

fun interface was never checked — two identically named ones merged into a
broken file silently. Functions were never checked either, on the grounds that
same-named functions might be overloads. True, but only when their parameter
types
differ.

And it had a false positive (D1)

Classes, type aliases and properties went into one pool of names. Kotlin keeps
types and values in separate namespaces, so class Foo and val Foo may
coexist — and were rejected. Fixed here as a consequence of the split.

The new rule

Walk the file, each class body and each companion body as its own scope. Per
scope, three tallies:

Namespace Holds Rule
types classes, fun interfaces, type aliases unique by name
values properties, val/var constructor parameters unique by name
functions functions unique by name and parameter types

A plain non-property constructor parameter is constructor-local and declares
nothing, so it cannot collide with a member — tested.

Raw blocks get their own rule

Raw blocks are hoisted singletons keyed by name, so two fragments may
legitimately carry the same one — that is deduplication, not a mistake.
Identical blocks now collapse during merge; only genuinely differing blocks
sharing a name are reported. A hard error on any repeated name (the obvious
implementation) would have rejected generation that is correct today.

Documented limitation

There is no type resolver here, so parameter types are compared as written:
io.p.Foo and Foo are different keys even when they name the same type, and
fun <T> f(x: T) does not collide with fun <R> f(x: R). The check therefore
misses some real duplicates. It is a net, not a proof — and a net that never
catches a fish it shouldn't. This is on the check's doc comment, not just here.

Verification

All 70 pre-existing tests pass. Ten added, half of them negative — the legal
shapes each check must not fire on: class + property + function sharing a name,
genuine overloads, a plain constructor parameter, a nested class as its own
scope, identical Raw blocks merging.

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.

Pull request overview

Implements Step B2 of the validation umbrella by replacing the old top-level-only duplicate check with a scope-aware redeclaration validator that tracks Kotlin’s separate namespaces (types / values / functions) and adds merge-time deduplication semantics for Raw blocks.

Changes:

  • Replace DuplicateDeclaration with DuplicateType / DuplicateValue / DuplicateFunction / DuplicateRaw, and validate collisions per scope (file, class body, companion body).
  • Add function redeclaration detection keyed by (name, parameter types as written) to allow overloads.
  • Deduplicate identical Raw blocks during merge_files_with, and add tests covering the new rules (including negative/legal cases).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/validate.rs Reworks redeclaration validation to be per-scope and per-namespace; adds function signature-based collision detection and new Check variants.
src/file.rs Deduplicates identical Raw blocks during merge so only genuinely differing duplicates are surfaced by validation.
src/tests.rs Adds targeted tests for nested scopes, namespaces, overload rules, ctor-property vs member collisions, fun interface duplicates, and Raw dedup behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/validate.rs
Comment on lines +53 to +56
/// Two `Raw` blocks in one scope share a name. Identical ones are merged
/// rather than reported (see [`merge_files`](super::merge_files)), so this
/// means two different blocks are claiming one identity.
DuplicateRaw,
Comment thread src/validate.rs
Comment on lines +338 to 351
/// A class body is its own scope, and so is its companion's.
fn check_class_scope(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
let inner = format!("{scope}/{}", c.name);
check_scope(&c.members, c.ctor_params(), &inner, d);
if let Some(comp) = &c.companion {
// Only an explicitly named companion declares a type name in the
// enclosing scope. The implicit `Companion` is a name this crate
// supplies rather than one the model declares, so treating it as a
// declaration could fire on a generator that manages the collision
// itself.
let cscope = format!("{inner}/{}", comp.name.as_deref().unwrap_or("Companion"));
check_scope(&comp.members, &[], &cscope, d);
}
}
The duplicate check had three problems. It never looked inside class
bodies — where nearly all generated declarations live; one generated
file in the downstream consumer has 43 functions and none at top level.
It did not check `fun interface` at all, so two of them merged into a
broken file silently. And it did not check functions at all, on the
grounds that same-named functions might be overloads — true only when
their parameter types differ.

It also had a false positive: classes, type aliases and properties went
into one pool of names, but Kotlin keeps types and values in separate
namespaces, so `class Foo` and `val Foo` may coexist and were rejected.
That is D1, fixed here as a consequence of the split.

Now: walk the file, each class body and each companion body as its own
scope, and keep three tallies per scope — types (classes, fun
interfaces, type aliases), values (properties and `val`/`var`
constructor parameters), functions (by name *and* parameter types). A
plain non-property constructor parameter is constructor-local and
declares nothing.

Raw blocks get a rule of their own. They are hoisted singletons keyed by
name, so two fragments may legitimately carry the same one; identical
blocks now collapse during merge, and only genuinely differing blocks
sharing a name are reported.

Limitation, documented on the check: parameter types are compared as
written, so `io.p.Foo` and `Foo` are different keys and generic renaming
does not collide. It misses some real duplicates. A net, not a proof.
@milyin
milyin force-pushed the step/b1-name-validity branch from f572b22 to 28cfcd4 Compare August 6, 2026 11:40
@milyin
milyin force-pushed the step/b2-scopes-and-namespaces branch from 4405e14 to 093b90d Compare August 6, 2026 11:40
@milyin
milyin changed the base branch from step/b1-name-validity to docs/validation-umbrella August 6, 2026 11:41
@milyin
milyin merged commit 5d63f41 into docs/validation-umbrella Aug 6, 2026
0 of 4 checks passed
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.

2 participants