schemagen: object types named Kind, Cat, Frame or SlotInfo are shadowed by RootBuilder's private member types (generated header does not compile) #77

Open
opened 2026-11-08 01:48:00 +00:00 by weaselbot · 0 comments
Member

Summary

weaseljson_schemagen.py derives a C++ struct/enum name for every object type from the JSON
property key (or $defs/definitions entry) via camel(). The generated RootBuilder class
declares four private member types

  enum class Kind : uint8_t { ... };   // gen.h:65
  enum class Cat { Reject, Str, Int, Dbl, Bool, Enum, Obj, Arr };
  struct SlotInfo { ... };
  struct Frame { ... };

(contrib/schemagen/weaseljson_schemagen.py:726, :1006, :1007, :1014), but
Builder._is_reserved() (contrib/schemagen/weaseljson_schemagen.py:271-273) only reserves
"Root" and "RootScalar":

    @staticmethod
    def _is_reserved(name):
        """Names generated internally that must not collide with user types."""
        return name in ("Root", "RootScalar")

So when a user type is named Kind, Cat, SlotInfo or Frame, the generated
RootBuilder::engage() (emitted by Emitter._engage,
contrib/schemagen/weaseljson_schemagen.py:781: auto *o = ({name} *)f.dest;) resolves the
type name to the member type instead of the namespace-scope struct, and the header does not
compile. This is reachable from ordinary, supported schemas — "kind" and "frame" are very
common JSON property names (e.g. Kubernetes objects all have a "kind" key).

Reproduction

kind.schema.json:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "kind": {
      "type": "object",
      "additionalProperties": false,
      "properties": { "x": { "type": "string" } }
    }
  }
}
python3 contrib/schemagen/weaseljson_schemagen.py kind.schema.json -o gen.h --namespace ns
printf '#include "gen.h"\nint main(){ ns::RootBuilder b; ns::Root r = b.take(); (void)r; }\n' > t.cpp
g++ -std=c++20 -fsyntax-only -I include -I . t.cpp

Actual result (generation succeeds, compiling the generated header fails):

gen.h: In member function 'void* ns::RootBuilder::engage(Frame&, bool)':
gen.h:414:25: error: request for member 'x' in '* o', which is of non-class type 'ns::RootBuilder::Kind'
  414 |       case 0: { if (!o->x) o->x.emplace(); return &*o->x; }

The generated header contains both a namespace-scope struct Kind { ... }; and, inside
class RootBuilder, enum class Kind : uint8_t { Root1, Kind };, so (Kind *)f.dest at
gen.h:412 refers to the enum.

Expected result: the header compiles (or the schema is rejected at generation time).

Other names that reproduce it

Same schema, just renaming the property (the value must be an object so that a struct with that
name is generated):

property key / $defs name error
"kind" (or $defs/Kind) request for member 'x' in '* o', which is of non-class type 'ns::RootBuilder::Kind'
"cat" ... of non-class type 'ns::RootBuilder::Cat'
"frame" 'struct ns::RootBuilder::Frame' has no member named 'x'
"slotInfo" (also "slot_info", camel() splits on _) 'struct ns::RootBuilder::SlotInfo' has no member named 'x'

($defs names work the same way, e.g. {"properties":{"k":{"$ref":"#/$defs/Kind"}}} fails
identically.)

Impact

A valid, supported schema produces a header that cannot be compiled, so a whole class of
realistic schemas (any object-typed property named kind, cat, frame, slotInfo, ...)
cannot be used at all. The failure is at C++ compile time and the diagnostics point at the
generated code, not at the schema, so it is hard to diagnose.

Suggested fix

Either extend _is_reserved() with "Kind", "Cat", "SlotInfo", "Frame" (the same way
"Root"/"RootScalar" are reserved), or — more robustly — give the builder's internal helper
types names that camel() can never produce. camel() always upper-cases the first character
(p[:1].upper() + p[1:]), so all generated type names start with an upper-case letter; lowercase
internal names such as kind_/cat_/slotInfo_/frame_ (and already-lowercase kWantKey)
are unreachable from user schemas.

Relation to existing issues

This is the same kind of defect as #69, but it is a different set of identifiers with a
different failure mechanism: #69 is about a user type redefining class RootBuilder at namespace
scope, whereas here the user type is shadowed inside RootBuilder by its private member types.
Reserving "RootBuilder" (the fix for #69) does not fix any of the names above, and "Kind" in
particular is also the enum emitted by Emitter._kind_enum(), so the four names need separate
entries in _is_reserved().

## Summary `weaseljson_schemagen.py` derives a C++ struct/enum name for every object type from the JSON property key (or `$defs`/`definitions` entry) via `camel()`. The generated `RootBuilder` class declares four **private member types** ```cpp enum class Kind : uint8_t { ... }; // gen.h:65 enum class Cat { Reject, Str, Int, Dbl, Bool, Enum, Obj, Arr }; struct SlotInfo { ... }; struct Frame { ... }; ``` (`contrib/schemagen/weaseljson_schemagen.py:726`, `:1006`, `:1007`, `:1014`), but `Builder._is_reserved()` (`contrib/schemagen/weaseljson_schemagen.py:271-273`) only reserves `"Root"` and `"RootScalar"`: ```python @staticmethod def _is_reserved(name): """Names generated internally that must not collide with user types.""" return name in ("Root", "RootScalar") ``` So when a user type is named `Kind`, `Cat`, `SlotInfo` or `Frame`, the generated `RootBuilder::engage()` (emitted by `Emitter._engage`, `contrib/schemagen/weaseljson_schemagen.py:781`: `auto *o = ({name} *)f.dest;`) resolves the type name to the *member* type instead of the namespace-scope struct, and the header does not compile. This is reachable from ordinary, supported schemas — `"kind"` and `"frame"` are very common JSON property names (e.g. Kubernetes objects all have a `"kind"` key). ## Reproduction `kind.schema.json`: ```json { "type": "object", "additionalProperties": false, "properties": { "kind": { "type": "object", "additionalProperties": false, "properties": { "x": { "type": "string" } } } } } ``` ```sh python3 contrib/schemagen/weaseljson_schemagen.py kind.schema.json -o gen.h --namespace ns printf '#include "gen.h"\nint main(){ ns::RootBuilder b; ns::Root r = b.take(); (void)r; }\n' > t.cpp g++ -std=c++20 -fsyntax-only -I include -I . t.cpp ``` Actual result (generation succeeds, compiling the generated header fails): ``` gen.h: In member function 'void* ns::RootBuilder::engage(Frame&, bool)': gen.h:414:25: error: request for member 'x' in '* o', which is of non-class type 'ns::RootBuilder::Kind' 414 | case 0: { if (!o->x) o->x.emplace(); return &*o->x; } ``` The generated header contains both a namespace-scope `struct Kind { ... };` and, inside `class RootBuilder`, `enum class Kind : uint8_t { Root1, Kind };`, so `(Kind *)f.dest` at gen.h:412 refers to the enum. Expected result: the header compiles (or the schema is rejected at generation time). ## Other names that reproduce it Same schema, just renaming the property (the value must be an object so that a struct with that name is generated): | property key / `$defs` name | error | |---|---| | `"kind"` (or `$defs/Kind`) | `request for member 'x' in '* o', which is of non-class type 'ns::RootBuilder::Kind'` | | `"cat"` | `... of non-class type 'ns::RootBuilder::Cat'` | | `"frame"` | `'struct ns::RootBuilder::Frame' has no member named 'x'` | | `"slotInfo"` (also `"slot_info"`, `camel()` splits on `_`) | `'struct ns::RootBuilder::SlotInfo' has no member named 'x'` | (`$defs` names work the same way, e.g. `{"properties":{"k":{"$ref":"#/$defs/Kind"}}}` fails identically.) ## Impact A valid, supported schema produces a header that cannot be compiled, so a whole class of realistic schemas (any object-typed property named `kind`, `cat`, `frame`, `slotInfo`, ...) cannot be used at all. The failure is at C++ compile time and the diagnostics point at the generated code, not at the schema, so it is hard to diagnose. ## Suggested fix Either extend `_is_reserved()` with `"Kind"`, `"Cat"`, `"SlotInfo"`, `"Frame"` (the same way `"Root"`/`"RootScalar"` are reserved), or — more robustly — give the builder's internal helper types names that `camel()` can never produce. `camel()` always upper-cases the first character (`p[:1].upper() + p[1:]`), so all generated type names start with an upper-case letter; lowercase internal names such as `kind_`/`cat_`/`slotInfo_`/`frame_` (and already-lowercase `kWantKey`) are unreachable from user schemas. ## Relation to existing issues This is the same *kind* of defect as #69, but it is a different set of identifiers with a different failure mechanism: #69 is about a user type redefining `class RootBuilder` at namespace scope, whereas here the user type is shadowed *inside* `RootBuilder` by its private member types. Reserving `"RootBuilder"` (the fix for #69) does not fix any of the names above, and `"Kind"` in particular is also the enum emitted by `Emitter._kind_enum()`, so the four names need separate entries in `_is_reserved()`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#77