diff --git a/README.md b/README.md index 280abbc..4f047c4 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,18 @@ Note the contract version: this raised `schemaVersion` to **2.0.0**, and it is a Additive in 3.0.0, so a 2.x consumer can ignore them: `unknownValue` on enum-accessor fields; `syntheticName` / `bitPosition` in the enum catalog; and the catalog itself growing from 328 to 403 entries so every `enumRef` resolves against it — 75 of the 87 referenced `(module, name)` pairs were in no catalog before. -Anything the extractor sees but cannot resolve structurally is counted under `manifest.diagnostics.iq.dropsByReason` rather than omitted, so "no constraint here" and "a constraint we failed to extract" never look alike. `manifest.diagnostics.iq.constraints`, `diagnostics.iq.targets.resolved` and `diagnostics.notif.actions` are floor-guarded: a WA refactor that hides one of these constructs fails the update instead of silently emptying a field. The unresolved states are guarded the other way — `scripts/lint-ir.py` pins the count of unaddressed requests and of unjudged accessors to an exact baseline. A rise means a constraint is being lost; a fall means extraction improved and the baseline owes an update. Either way the lint fails, so neither direction passes unnoticed. +### Enough to *call* WhatsApp's own builder, not only to encode the stanza yourself + +Everything above describes the wire. That serves a client that encodes the stanza itself — it needs to know a group create carries a `` of type `user_jid`. It does not serve a client that **runs WhatsApp's own modules**, which needs the other half: that the value goes in `args.participantArgs[].participantJid`. Neither half implies the other — WA picks the argument key independently of the attribute it lands in (`subjectElementValue` becomes the *text* of ``) — and almost every request is composed out of mixins, so the argument key is usually defined in a different module from the tag it fills. So the request side also carries the builder: + +- **Argument paths** (`argPath` on a request node, an attribute, or an element content) — the absolute path from the builder's argument object, as segments. `list` marks each segment a repeated combinator iterates: `REPEATED_CHILD(template, list, min, max)` calls the template once per element, while `OPTIONAL_CHILD`/`HAS_OPTIONAL_CHILD` hand the object over whole. Nested repeats mark more than one — `userArgs[] → deviceArgs[] → deviceId` is a key read off an element of an element. The same suffix in the wrong place writes the value where the vendor builder never reads it, and the stanza goes out without it. Recovered structurally — from the function's single argument parameter and the `var x = .` destructure — never from a name: `…Args`, `has…` and `any…` are WA conventions that make the IR readable, not evidence. A path that isn't structurally recoverable is absent and counted under `manifest.diagnostics.iq.builder`, never guessed. The legacy `WAWeb*Job` builders take positional parameters rather than one options object, so they get no path at all and are counted as such. +- **Cardinality of a request child** (`presence`, plus `repeatMin`/`repeatMax`) — the three states `WASmaxChildren` distinguishes, of which the wire shows one. `` on a group create is a *presence marker*: its template takes no arguments, its whole meaning is being there, and a consumer can model it as a `bool` — which it must not do for an optional child and cannot do for an empty required one, all three of which are the same empty element on the wire. The repeat bounds are the ones a server enforces (`add/participant` 1..1024, `query/group` 1..10000, `media_list` 0..10); a `repeatMin` with no `repeatMax` is WA's explicit `1/0`, i.e. unbounded above, which stays distinguishable from a child that states no bound at all and has neither. +- **The addressee's argument key** (`targetArgPath` on a request) — `target` says a request is sent to one group's own JID rather than to a server, and a consumer running the vendor builder still has to know which argument supplies it. 30 of the 31 runtime-addressed requests carry it (`iqTo`, and one composed through a mixin group as `baseGetGroupOrServerMixinGroupArgs → baseGetGroup → iqTo`); the one that does not is a legacy `WAWeb*Job` builder, which takes positional parameters and so has no argument object for a path to point into. +- **Element values that survive the mixin boundary** (`content`) — `smax("subject", null, subjectElementValue)` is the entire payload of a group rename. WA's builders bind that payload to a local before writing it, and a bare local used to be ignored outright in case it was a node variable. It is now told apart structurally: a local that resolves to an argument path is content, one that resolves to a `smax(…)` call is a child. + +**`schemaVersion` is now 4.0.0.** Nearly all of the above is additive — every new property is optional and skipped at its default, and each committed `*/index.json` validates clean against its own **3.0.0** schema (0 errors across all 12 domains). One change is not, and it is the whole reason for the major: `value` on a request attribute was documented as present only for `kind: "const"`, and now also carries the fixed literal of a `WASmaxAttrs.OPTIONAL_LITERAL(lit, flag)` attribute, whose `kind` is `optional`. Migration: read `value` as "what this attribute says **when** it is written" rather than as an unconditional constant — on an `optional` attribute the builder writes it only when its boolean gate is set, which is the attribute analogue of a presence marker. Eight committed IQ attributes are in that state, and the old JSON Schema accepts every one of them, which is precisely why the version has to say what the schema cannot. Additive alongside it: `argPath`, `presence`, `repeatMin`/`repeatMax`, `targetArgPath`, and `content` populated on 45 request nodes that previously carried none. + +Anything the extractor sees but cannot resolve structurally is counted under `manifest.diagnostics.iq.dropsByReason` rather than omitted, so "no constraint here" and "a constraint we failed to extract" never look alike. `manifest.diagnostics.iq.constraints`, `diagnostics.iq.targets.resolved`, `diagnostics.iq.builder` and `diagnostics.notif.actions` are floor-guarded: a WA refactor that hides one of these constructs fails the update instead of silently emptying a field. The unresolved states are guarded the other way — `scripts/lint-ir.py` pins the count of unaddressed requests and of unjudged accessors to an exact baseline. A rise means a constraint is being lost; a fall means extraction improved and the baseline owes an update. Either way the lint fails, so neither direction passes unnoticed. ## Quick start diff --git a/crates/wa-codegen/src/emit.rs b/crates/wa-codegen/src/emit.rs index 9144826..e16f5b9 100644 --- a/crates/wa-codegen/src/emit.rs +++ b/crates/wa-codegen/src/emit.rs @@ -2465,6 +2465,7 @@ mod tests { value: value.map(|v| v.into()), required: false, enum_ref: None, + arg_path: None, } } fn leaf(tag: &str) -> WapChildNode { @@ -2475,6 +2476,7 @@ mod tests { content: None, repeats: false, variant_groups: vec![], + ..Default::default() } } @@ -2563,6 +2565,7 @@ mod tests { content: None, repeats: true, variant_groups: vec![], + ..Default::default() }; let node = WapChildNode { tag: "config".into(), @@ -2577,6 +2580,7 @@ mod tests { children: vec![item], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -2887,6 +2891,7 @@ mod tests { content: None, repeats: false, variant_groups: groups, + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3005,6 +3010,7 @@ mod tests { content: None, repeats: true, variant_groups: vec![], + ..Default::default() }; let node = WapChildNode { tag: "config".into(), @@ -3019,6 +3025,7 @@ mod tests { children: vec![kid("type"), kid("2fa")], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3056,6 +3063,7 @@ mod tests { children: vec![kid("type")], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3089,6 +3097,7 @@ mod tests { children: vec![kid("my-tag")], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3121,6 +3130,7 @@ mod tests { content: None, repeats: true, variant_groups: vec![], + ..Default::default() }; let node = WapChildNode { tag: "config".into(), @@ -3135,6 +3145,7 @@ mod tests { children: vec![kid("foo-bar"), kid("foo_bar")], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3173,6 +3184,7 @@ mod tests { children: vec![kid("item")], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let mut ctx = VariantCtx { @@ -3205,6 +3217,7 @@ mod tests { content: None, repeats: true, variant_groups: vec![], + ..Default::default() }; let node = WapChildNode { tag: "config".into(), @@ -3219,6 +3232,7 @@ mod tests { children: vec![item], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3265,6 +3279,7 @@ mod tests { children: vec![nested], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3310,6 +3325,7 @@ mod tests { children: vec![nested], }], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3486,6 +3502,7 @@ mod tests { ], }, ], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3567,6 +3584,7 @@ mod tests { optional: false, variants: vec![mk("appid"), mk("voip"), mk("endpoint")], }], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3788,6 +3806,7 @@ mod tests { content: None, repeats: false, variant_groups: vec![], + ..Default::default() }; let (lines, var, _) = build1(&child); let code = lines.join("\n"); @@ -3820,6 +3839,7 @@ mod tests { content: None, repeats: false, variant_groups: vec![], + ..Default::default() }; let (lines, _, _) = build1(&parent); let code = lines.join("\n"); @@ -3850,6 +3870,7 @@ mod tests { }), repeats: false, variant_groups: vec![], + ..Default::default() }; let (lines, _, _) = build1(&node); let code = lines.join("\n"); @@ -3878,6 +3899,7 @@ mod tests { }), repeats: false, variant_groups: vec![], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3953,6 +3975,7 @@ mod tests { }), repeats: false, variant_groups: vec![], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); @@ -3984,6 +4007,7 @@ mod tests { }), repeats: false, variant_groups: vec![], + ..Default::default() }; let (mut enums, mut fields) = (Vec::new(), Vec::new()); let reserved = std::collections::HashSet::new(); diff --git a/crates/wa-codegen/src/lib.rs b/crates/wa-codegen/src/lib.rs index 3caaab2..5cf1ca3 100644 --- a/crates/wa-codegen/src/lib.rs +++ b/crates/wa-codegen/src/lib.rs @@ -252,6 +252,7 @@ mod tests { value: None, required: true, enum_ref: None, + arg_path: None, } } @@ -291,6 +292,7 @@ mod tests { exported_function: Some("title".into()), all_exports: vec!["title".into()], request: IqRequestDef { + target_arg_path: None, namespace: "fb:thrift_iq".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -347,6 +349,7 @@ mod tests { exported_function: Some("detail".into()), all_exports: vec!["detail".into()], request: IqRequestDef { + target_arg_path: None, namespace: "fb:thrift_iq".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -404,6 +407,7 @@ mod tests { exported_function: Some("weights".into()), all_exports: vec!["weights".into()], request: IqRequestDef { + target_arg_path: None, namespace: "fb:thrift_iq".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -455,6 +459,7 @@ mod tests { exported_function: Some("scores".into()), all_exports: vec!["scores".into()], request: IqRequestDef { + target_arg_path: None, namespace: "fb:thrift_iq".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -516,6 +521,7 @@ mod tests { exported_function: Some("keys".into()), all_exports: vec!["keys".into()], request: IqRequestDef { + target_arg_path: None, namespace: "encrypt".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -569,6 +575,7 @@ mod tests { exported_function: Some("digest".into()), all_exports: vec!["digest".into()], request: IqRequestDef { + target_arg_path: None, namespace: "encrypt".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -614,6 +621,7 @@ mod tests { exported_function: Some("pn".into()), all_exports: vec!["pn".into()], request: IqRequestDef { + target_arg_path: None, namespace: "w:g2".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -688,6 +696,7 @@ mod tests { exported_function: Some("blob".into()), all_exports: vec!["blob".into()], request: IqRequestDef { + target_arg_path: None, namespace: "encrypt".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -723,6 +732,7 @@ mod tests { exported_function: Some("queryTest".into()), all_exports: vec!["queryTest".into()], request: IqRequestDef { + target_arg_path: None, namespace: "w:test".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -736,6 +746,7 @@ mod tests { content: None, repeats: false, variant_groups: vec![], + ..Default::default() }], }, response: ParsedResponse { @@ -787,6 +798,7 @@ mod tests { exported_function: Some("queryMode".into()), all_exports: vec!["queryMode".into()], request: IqRequestDef { + target_arg_path: None, namespace: "w:mode".into(), iq_type: IqType::Get, target: IqTarget::Server, @@ -826,6 +838,7 @@ mod tests { exported_function: None, all_exports: vec![], request: IqRequestDef { + target_arg_path: None, namespace: "w:x".into(), iq_type: IqType::Set, target: IqTarget::GroupServer, diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index 8b591bb..2f2e0b1 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -1143,11 +1143,13 @@ mod tests { value: None, required: true, enum_ref: None, + arg_path: None, }], children: vec![], content: None, repeats: false, variant_groups: vec![], + ..Default::default() }; let children = vec![ node("participant", WapAttrKind::UserJid), @@ -1292,6 +1294,7 @@ mod tests { exported_function: exported.map(str::to_string), all_exports: vec![], request: IqRequestDef { + target_arg_path: None, namespace: "w:test".into(), iq_type: IqType::Get, target: IqTarget::Server, diff --git a/crates/wa-codegen/src/stanza_export.rs b/crates/wa-codegen/src/stanza_export.rs index 9c596c7..095d31d 100644 --- a/crates/wa-codegen/src/stanza_export.rs +++ b/crates/wa-codegen/src/stanza_export.rs @@ -282,6 +282,7 @@ mod tests { module: module.into(), variants: vars, }), + arg_path: None, } } @@ -292,6 +293,7 @@ mod tests { value: None, required: true, enum_ref: None, + arg_path: None, } } diff --git a/crates/wa-ir/src/iq.rs b/crates/wa-ir/src/iq.rs index 065b8ba..07bae4a 100644 --- a/crates/wa-ir/src/iq.rs +++ b/crates/wa-ir/src/iq.rs @@ -45,6 +45,81 @@ pub enum WapAttrKind { Dynamic, } +/// One step of a [`WapArgPath`] — a key in the builder's argument object, plus +/// whether that key holds the **list** a repeated child iterates. +/// +/// `list` is set on one kind of segment: the array argument of a +/// `WASmaxChildren.REPEATED_CHILD(template, list, min, max)` call, which invokes the +/// template once per element. It is *not* set for `OPTIONAL_CHILD`/`HAS_OPTIONAL_CHILD`, +/// where the argument object is handed to the template as-is. A path may carry several, +/// one per repeated combinator it passes through — `userArgs[] → deviceArgs[] → deviceId` +/// is read off an element of an element. The distinction is +/// load-bearing rather than cosmetic: writing `participantArgs.participantJid` where the +/// builder reads `participantArgs[0].participantJid` puts the value somewhere the vendor +/// builder never looks, and the stanza goes out without it. +/// +/// It does not say how long the list may be — that is [`WapChildNode::repeat_min`] / +/// [`repeat_max`] on the node the path addresses — and it does not say the key is +/// present at runtime. +/// +/// [`repeat_max`]: WapChildNode::repeat_max +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct WapArgSegment { + /// The property name read off the argument object (`participantArgs`, `iqTo`, …). + pub key: String, + /// This key holds the array a `REPEATED_CHILD` iterates; the next segment (and + /// everything below it) is read off an *element*, not off the array. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub list: bool, +} + +/// Where a value lives in the **argument object of WhatsApp's own request builder** — +/// an absolute path from the builder's single parameter, e.g. +/// `participantArgs[] → participantJid` for `makeCreateRequest({participantArgs: [{ +/// participantJid: … }]})`. +/// +/// This describes the *builder*, not the wire. A consumer that encodes the stanza itself +/// has no use for it and should read `tag`/`attrs`/`content`; a consumer that calls the +/// vendor builder needs it, because the argument key is frequently not derivable from +/// the wire name it ends up in (`subjectElementValue` → the content of ``). +/// +/// What it does **not** guarantee: that the key is required (see +/// [`WapChildNode::presence`]), that a value written there is valid, or that a path +/// exists at all — an unrecoverable path is absent from the IR and counted under +/// `manifest.diagnostics.iq.argPaths`, never guessed. +pub type WapArgPath = Vec; + +/// Whether a request child node must be built, may be omitted, or exists only to +/// signal a flag — the three states `WASmaxChildren` distinguishes and the wire does +/// not. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "snake_case")] +pub enum WapChildPresence { + /// Built unconditionally — the builder calls the child template directly, or + /// `REPEATED_CHILD` iterates a list that is itself a required argument. For a + /// repeated child, "required" is about the *list argument*: a `repeat_min` of 0 + /// still allows the list to be empty. + #[default] + Required, + /// `WASmaxChildren.OPTIONAL_CHILD(template, args)` — emitted only when the + /// argument object is supplied. The child carries real attrs/content when present. + Optional, + /// `WASmaxChildren.HAS_OPTIONAL_CHILD(template, flag)` — a **presence marker**: + /// the template takes no arguments, so the element is empty and its entire meaning + /// is being there (`` on a group create). A consumer can model it as a + /// `bool` argument, which it cannot do for [`Optional`] and must not do for an + /// empty [`Required`] child. + /// + /// It does not guarantee the element has no attributes on the wire — only that + /// this builder writes none. + /// + /// [`Optional`]: WapChildPresence::Optional + PresenceFlag, +} + /// A single attribute on a request stanza node. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -52,7 +127,19 @@ pub enum WapAttrKind { pub struct WapAttrDef { pub name: String, pub kind: WapAttrKind, - /// Present only for [`WapAttrKind::Const`]. + /// The fixed wire value the builder writes itself. + /// + /// Present for a [`WapAttrKind::Const`] attribute, and for an + /// [`Optional`](WapAttrKind::Optional) one built by + /// `WASmaxAttrs.OPTIONAL_LITERAL(lit, flag)` — there the value is this literal and + /// the argument is a boolean deciding whether the attribute is written at all, the + /// attribute analogue of [`WapChildPresence::PresenceFlag`]. Such an attribute + /// carries no [`arg_path`]: there is no address for a value the builder supplies, and + /// pointing at the boolean would tell a consumer to put the wire string there. + /// + /// It does not say the attribute is always sent — only what it says when it is. + /// + /// [`arg_path`]: WapAttrDef::arg_path #[serde(default, skip_serializing_if = "Option::is_none")] pub value: Option, pub required: bool, @@ -65,6 +152,12 @@ pub struct WapAttrDef { /// a variable, or any non-enum expression (never guessed from value coincidence). #[serde(default, skip_serializing_if = "Option::is_none")] pub enum_ref: Option, + /// Where this attribute's value comes from in the builder's argument object — an + /// absolute [`WapArgPath`]. Absent for a [`WapAttrKind::Const`] (the builder writes + /// the literal itself, there is no argument) and whenever the path is not + /// structurally recoverable; those are counted, not guessed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub arg_path: Option, } /// A wire enum a request attribute's value is drawn from, recovered by resolving a @@ -91,6 +184,60 @@ pub struct AttrEnumVariant { pub value: String, } +impl WapChildPresence { + /// Whether this is the default (unconditional) presence — the serde skip predicate, + /// so the common case costs no bytes in the emitted document. + pub fn is_required(&self) -> bool { + matches!(self, WapChildPresence::Required) + } +} + +impl WapAttrDef { + /// Whether this attribute's wire value comes from a caller argument rather than from + /// the builder itself — so an absent [`arg_path`] is an extraction gap rather than + /// nothing to address. A constant, a generated id, and an attribute with a recorded + /// fixed [`value`] (WA's `OPTIONAL_LITERAL`, whose argument is a presence flag) all + /// read none. + /// + /// Says nothing about whether the value is required, only about where it comes from. + /// + /// [`arg_path`]: WapAttrDef::arg_path + /// [`value`]: WapAttrDef::value + pub fn reads_argument(&self) -> bool { + !matches!(self.kind, WapAttrKind::Const | WapAttrKind::GeneratedId) && self.value.is_none() + } +} + +impl WapContent { + /// Whether this payload comes from a caller argument rather than from the builder — + /// the same question [`WapAttrDef::reads_argument`] asks, for element content. Only a + /// [`WapContentKind::Const`] reads none: the builder writes that literal itself. + /// + /// A [`const_bytes`] payload still does. It is *pinned* rather than written — every + /// call site in the bundle passes the same compile-time constant — so the argument + /// exists and a consumer calling the builder must supply it. The two facts answer + /// different questions: [`arg_path`] says where the value goes, `const_bytes` says + /// what to put there. + /// + /// [`const_bytes`]: WapContent::const_bytes + /// [`arg_path`]: WapContent::arg_path + pub fn reads_argument(&self) -> bool { + self.kind != WapContentKind::Const + } +} + +impl WapChildNode { + /// Whether a `WASmaxChildren` combinator fed this child — `REPEATED_CHILD`, + /// `OPTIONAL_CHILD` or `HAS_OPTIONAL_CHILD` — so it has an argument of its own for + /// [`arg_path`] to address. A child built unconditionally in the builder body has no + /// argument object behind it and is not missing an address by lacking one. + /// + /// [`arg_path`]: WapChildNode::arg_path + pub fn is_combinator_fed(&self) -> bool { + self.repeats || !self.presence.is_required() + } +} + /// The kind of leaf payload a request node carries in its element content /// (`wap("id", null, BIG_ENDIAN_CONTENT(x, 3))` → the `` node's content). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -157,6 +304,18 @@ pub struct WapContent { /// [`value`]: WapContent::value #[serde(default, skip_serializing_if = "Option::is_none")] pub value_source: Option, + /// Where the content value comes from in the builder's argument object — an + /// absolute [`WapArgPath`] (``'s text is `subjectElementValue`). Absent for a + /// [`WapContentKind::Const`] payload, which the builder writes itself, and whenever + /// the path is not structurally recoverable. + /// + /// A [`const_bytes`] payload keeps its path: it is *pinned* — every call site passes + /// the same constant — not written by the builder, so the argument is still read and + /// a consumer still has to supply it. The constant says what to put here. + /// + /// [`const_bytes`]: WapContent::const_bytes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub arg_path: Option, } /// A node in a request stanza tree. @@ -171,10 +330,53 @@ pub struct WapChildNode { /// The leaf element content, when this node carries a value instead of child /// nodes (``, ``, `` in a prekey ``). `None` for /// container nodes and attr-only nodes. + /// + /// Says what the element carries when the builder writes it, not that it is always + /// written: a payload contributed by an `optionalMerge` onto a node built elsewhere + /// is stated here even though that merge can be skipped. There is no per-content + /// optionality in this contract — [`presence`] is a property of the node. + /// + /// [`presence`]: WapChildNode::presence #[serde(default, skip_serializing_if = "Option::is_none")] pub content: Option, /// Whether this child can appear multiple times (maps to `Vec<_>` in codegen). pub repeats: bool, + /// Whether the builder emits this child unconditionally, only when its arguments + /// are supplied, or only as a presence marker. See [`WapChildPresence`]; defaults + /// to [`WapChildPresence::Required`], which is also what a node whose call site was + /// not one of the `WASmaxChildren` combinators gets. + #[serde(default, skip_serializing_if = "WapChildPresence::is_required")] + pub presence: WapChildPresence, + /// Lower bound on the number of repetitions, from the 3rd argument of + /// `REPEATED_CHILD(template, list, min, max)`. Present only when [`repeats`] is set + /// and the bound is a literal; a computed or non-finite bound is omitted and + /// counted, never defaulted to 0. + /// + /// It is the bound *this builder* enforces before sending. The server enforces its + /// own, which may be stricter. + /// + /// [`repeats`]: WapChildNode::repeats + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repeat_min: Option, + /// Upper bound on the number of repetitions — the 4th argument of `REPEATED_CHILD` + /// (`add/participant` caps at 1024, `query/group` at 10000). Same presence rules as + /// [`repeat_min`]. + /// + /// [`repeat_min`]: WapChildNode::repeat_min + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repeat_max: Option, + /// Where this node's own argument object lives in the builder's arguments — the + /// list for a [`repeats`] child (its last segment carries [`WapArgSegment::list`]), + /// the argument object for an [`WapChildPresence::Optional`] one, the boolean for a + /// [`WapChildPresence::PresenceFlag`] one. + /// + /// Absent for a node the builder constructs inline: such a node has no argument + /// object of its own, and its attrs/content carry absolute paths regardless, so a + /// consumer never has to concatenate. + /// + /// [`repeats`]: WapChildNode::repeats + #[serde(default, skip_serializing_if = "Option::is_none")] + pub arg_path: Option, /// Mutually-exclusive variant groups this node can take, each from a smax /// MixinGroup disjunction (e.g. newsletter params: a `jid` variant XOR an /// `invite` variant, discriminated by `type`). Exactly one variant of each @@ -312,6 +514,19 @@ pub struct IqRequestDef { pub namespace: String, pub iq_type: IqType, pub target: IqTarget, + /// Where the addressee comes from in the builder's argument object, when the builder + /// writes a `to` it reads from an argument rather than from a constant. + /// + /// [`target`] says WHAT kind of addressee the request takes — a group's own JID, the + /// group server, `s.whatsapp.net` — and for the runtime ones a consumer calling the + /// vendor builder still has to know where to put it. Absent when the addressee is a + /// compile-time constant (nothing to supply), when the builder writes no `to`, and + /// when the path is not structurally recoverable, which is counted like every other + /// missing address rather than guessed. + /// + /// [`target`]: IqRequestDef::target + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_arg_path: Option, pub children: Vec, } @@ -1121,6 +1336,7 @@ mod tests { exported_function: None, all_exports: vec![], request: IqRequestDef { + target_arg_path: None, namespace: "w:foo".into(), iq_type: IqType::Get, target: IqTarget::Server, diff --git a/crates/wa-ir/src/lib.rs b/crates/wa-ir/src/lib.rs index fc1531e..2c7e257 100644 --- a/crates/wa-ir/src/lib.rs +++ b/crates/wa-ir/src/lib.rs @@ -120,7 +120,41 @@ pub enum Scalar { /// [`ParsedFieldType`]: crate::ParsedFieldType /// [`parserRequired`]: crate::ParsedField::parser_required /// [`ParsedField::unknown_value`]: crate::ParsedField::unknown_value -pub const SCHEMA_VERSION: &str = "3.0.0"; +/// +/// # 4.0.0 — the builder side of a request +/// +/// The IR gained a dimension it did not have: where a value goes in the argument object +/// of WA's own request builder ([`WapArgSegment`], on nodes, attributes, element contents +/// and the request's addressee), the cardinality of a request child +/// ([`WapChildPresence`], plus [`WapChildNode::repeat_min`]/[`repeat_max`]), and element +/// contents that used to stop at the mixin boundary. +/// +/// Nearly all of it is additive — every new property is optional and skipped at its +/// default, and each committed `*/index.json` validates clean against its own 3.0.0 +/// schema, 0 errors across all 12 domains. **One change is not**, and it is why this is a +/// major rather than a minor: +/// +/// - [`WapAttrDef::value`] was documented as present only for [`WapAttrKind::Const`], and +/// now also carries the fixed literal of an `OPTIONAL_LITERAL` attribute, whose `kind` +/// is [`Optional`](WapAttrKind::Optional). A 3.0 consumer reading a present `value` as +/// an unconditional constant would send that attribute always; it is written only when +/// the builder's boolean gate says so. Migration: treat `value` on a non-`Const` +/// attribute as "this is what it says WHEN it is written", and read +/// [`WapChildPresence`]'s attribute analogue from the `kind`. Eight committed IQ +/// attributes are in this state. The old JSON Schema accepts them, which is exactly why +/// the version has to say what the schema cannot. +/// +/// What else changed *value* rather than shape: `content` is now populated on 45 request +/// nodes that previously had none — an optional field filled in where the builder does +/// supply a payload. +/// +/// [`WapArgSegment`]: crate::WapArgSegment +/// [`WapChildPresence`]: crate::WapChildPresence +/// [`WapChildNode::repeat_min`]: crate::WapChildNode::repeat_min +/// [`repeat_max`]: crate::WapChildNode::repeat_max +/// [`WapAttrDef::value`]: crate::WapAttrDef::value +/// [`WapAttrKind::Const`]: crate::WapAttrKind::Const +pub const SCHEMA_VERSION: &str = "4.0.0"; /// Envelope that stamps a domain IR document with [`SCHEMA_VERSION`] at emit /// time, without altering the inner document's shape. diff --git a/crates/wa-scan/src/alias.rs b/crates/wa-scan/src/alias.rs index d19f633..6e42cb8 100644 --- a/crates/wa-scan/src/alias.rs +++ b/crates/wa-scan/src/alias.rs @@ -45,6 +45,29 @@ impl AliasMap { self.map.get(name).copied() } + /// This map layered over `outer`: every alias `outer` knows that this body does not + /// bind itself, plus this map's own. + /// + /// A function body re-parsed on its own sees only the aliases it declares itself, + /// while the module-level `(X = o("WASmaxAttrs"))` it actually calls through lives in + /// the enclosing program. Layering restores the lexical view. + /// + /// `shadowed` is every name the body BINDS — parameters and declarations — whether or + /// not it binds them to something tracked. Those names are dropped from the outer map + /// rather than inherited: the minifier reuses `n`, `t` and `a` everywhere, so a + /// callback whose parameter is spelled like the module's `WASmaxAttrs` alias would + /// otherwise have its own element read as that builder. + pub(crate) fn over(&self, outer: &AliasMap, shadowed: &HashSet) -> AliasMap { + let mut map: HashMap = outer + .map + .iter() + .filter(|(k, _)| !shadowed.contains(*k)) + .map(|(k, v)| (k.clone(), *v)) + .collect(); + map.extend(self.map.iter().map(|(k, v)| (k.clone(), *v))); + AliasMap { map } + } + #[cfg(test)] fn is_empty(&self) -> bool { self.map.is_empty() @@ -83,6 +106,58 @@ pub(crate) fn resolve_owner(e: &Expression, aliases: &AliasMap) -> Option<&'stat None } +/// Every name this program's OWN scope binds: `var`/`let`/`const` declarators, and the +/// names of the functions and classes it declares. Not an alias map — a binding is +/// recorded whatever it is bound to, which is the point: an untracked local still shadows +/// an outer alias of the same name. +/// +/// Nested functions and classes are named but not entered. A `function helper(A){…}` +/// inside the body binds `A` in ITS scope, not in this one, so treating it as a shadow +/// here would unresolve a perfectly good outer alias for the body's own calls. The +/// nested body, when it is scanned in turn, gets its own set. +pub(crate) fn bound_names(program: &oxc_ast::ast::Program) -> HashSet { + struct Binder { + names: HashSet, + } + impl Binder { + fn declare(&mut self, id: Option<&oxc_ast::ast::BindingIdentifier>) { + if let Some(id) = id { + self.names.insert(id.name.to_string()); + } + } + } + impl<'a> Visit<'a> for Binder { + fn visit_binding_identifier(&mut self, id: &oxc_ast::ast::BindingIdentifier<'a>) { + self.names.insert(id.name.to_string()); + } + + // A declaration's NAME is bound here; everything inside it — parameters and + // locals — belongs to the scope it opens. + fn visit_function( + &mut self, + func: &oxc_ast::ast::Function<'a>, + _flags: oxc_syntax::scope::ScopeFlags, + ) { + self.declare(func.id.as_ref()); + } + + fn visit_arrow_function_expression( + &mut self, + _arrow: &oxc_ast::ast::ArrowFunctionExpression<'a>, + ) { + } + + fn visit_class(&mut self, class: &oxc_ast::ast::Class<'a>) { + self.declare(class.id.as_ref()); + } + } + let mut b = Binder { + names: HashSet::new(), + }; + b.visit_program(program); + b.names +} + /// Build the [`AliasMap`] for a parsed module program. pub(crate) fn build_alias_map(program: &oxc_ast::ast::Program) -> AliasMap { let mut b = AliasBuilder { diff --git a/crates/wa-scan/src/attrs.rs b/crates/wa-scan/src/attrs.rs index 78f34b1..e00ab9a 100644 --- a/crates/wa-scan/src/attrs.rs +++ b/crates/wa-scan/src/attrs.rs @@ -191,6 +191,7 @@ fn classify_attr_node<'a>( value: val, required, enum_ref: None, + arg_path: None, }; // A pending enum link — only its (name, module) are known here; the variants are // resolved cross-module after the scan (see `crate::enum_link`), which then clears @@ -211,6 +212,7 @@ fn classify_attr_node<'a>( module, variants: Vec::new(), }), + arg_path: None, } }; @@ -232,7 +234,23 @@ fn classify_attr_node<'a>( if matches!(method, "OPTIONAL" | "OPTIONAL_LITERAL") && callee_object(call).and_then(|o| resolve_owner(o, aliases)) == Some("WASmaxAttrs") { - return owned(WapAttrKind::Optional, None, false); + // `OPTIONAL_LITERAL(lit, flag)` differs from `OPTIONAL(kind, val)` in what its + // argument IS: the value is the fixed literal, and the argument is a boolean + // deciding whether the attribute is written at all — the attribute analogue of + // `HAS_OPTIONAL_CHILD`. Recording the literal is what lets the two be told + // apart in the document: without it, an optional attribute with no `argPath` + // reads the same whether its value is a builder constant or something the + // extractor failed to address. + let literal = (method == "OPTIONAL_LITERAL") + .then(|| { + call.arguments + .first() + .and_then(arg_expr) + .and_then(as_string_lit) + }) + .flatten() + .map(str::to_string); + return owned(WapAttrKind::Optional, literal, false); } // FORM A: `CUSTOM_STRING(o("Mod").EnumName.VARIANT)` — the wire value is drawn // from that enum. Gated on `CUSTOM_STRING` (a wire builder) so an unrelated diff --git a/crates/wa-scan/src/enum_link.rs b/crates/wa-scan/src/enum_link.rs index b1732ac..fc19e3a 100644 --- a/crates/wa-scan/src/enum_link.rs +++ b/crates/wa-scan/src/enum_link.rs @@ -305,6 +305,7 @@ mod tests { module: module.into(), variants: Vec::new(), }), + arg_path: None, } } diff --git a/crates/wa-scan/src/helper_index.rs b/crates/wa-scan/src/helper_index.rs index 163193d..09d3fbb 100644 --- a/crates/wa-scan/src/helper_index.rs +++ b/crates/wa-scan/src/helper_index.rs @@ -34,6 +34,19 @@ impl HelperIndex { } } +#[cfg(test)] +impl HelperIndex { + /// Seed one entry, so a test can exercise the cross-module branch of + /// [`crate::request::resolve_child_node`] without building a whole bundle. + pub(crate) fn with(module: &str, func: &str, tree: Vec) -> Self { + let mut index = HelperIndex::default(); + index + .by_qualified_name + .insert(qualified(module, func), tree); + index + } +} + fn qualified(module: &str, func: &str) -> String { format!("{module}.{func}") } diff --git a/crates/wa-scan/src/mixin_index.rs b/crates/wa-scan/src/mixin_index.rs index 3055770..fdac279 100644 --- a/crates/wa-scan/src/mixin_index.rs +++ b/crates/wa-scan/src/mixin_index.rs @@ -32,11 +32,34 @@ use crate::attrs::{extract_attrs_from_obj, parse_wap_call}; use crate::helper_index::HelperIndex; use crate::module::{iq_type_from_merge_name, require_module_name}; use crate::request::{ - MixinContributions, VarScope, build_var_scope, resolve_child_node, resolve_contribution, + MergeArg, MixinContributions, VarScope, build_var_scope, resolve_child_node, + resolve_contribution, }; use wa_oxc::{arg_expr, callee_method, callee_object}; use wa_transform::ModuleDefinition; +/// A mixin folded in by another, with the argument the folding frame handed it. +/// +/// The pair is what lets a fold rebase: the module says WHAT is contributed, the +/// [`MergeArg`] says where in the calling frame's argument object it was read from. +#[derive(Clone, Debug)] +pub(crate) struct MergeCallee { + pub module: String, + pub arg: MergeArg, +} + +/// A callee handed the caller's whole argument object — the ordinary shape, and the one +/// tests use when the chain's prefixes are not what they exercise. +#[cfg(test)] +impl From<&str> for MergeCallee { + fn from(module: &str) -> Self { + MergeCallee { + module: module.to_string(), + arg: MergeArg::Whole, + } + } +} + /// What one mixin contributes to the `` it helps build. #[derive(Clone, Default, Debug)] pub(crate) struct MixinIqFragment { @@ -50,10 +73,18 @@ pub(crate) struct MixinIqFragment { /// all — distinct from [`IqTarget::Unknown`], which is a `to` that resolved to no /// server. pub target: Option, - /// Other `WASmaxOut…` mixins this mixin folds in (by module name), in source - /// order with duplicates removed — for transitive resolution (e.g. a Hack - /// mixin whose `type` comes from a Base mixin it calls). - pub merged_callees: Vec, + /// Where this mixin reads the addressee from, when it reads one from its own argument + /// object — relative to the mixin's frame, so the merge chain prefixes it like + /// everything else it contributes. Most runtime addressees live here rather than in + /// the request builder: a newsletter mixin writes `to: JID(args.newsletterId)` and the + /// request only folds it in. + pub target_arg_path: Option, + /// Other `WASmaxOut…` mixins this mixin folds in, in source order — for transitive + /// resolution (e.g. a Hack mixin whose `type` comes from a Base mixin it calls), each + /// with the argument this mixin handed it so the fold can rebase what it contributes. + /// A module merged twice appears twice: the two calls may hand it different objects, + /// and collapsing them here would silently keep the first one's address. + pub merged_callees: Vec, /// The children the mixin's inner `smax("iq", …, children)` fragment adds to /// the `` (e.g. `BaseReportMixin` → `spam_list{spam_flow}`). Merged by tag /// into a Request's children so cross-module attrs/children aren't lost. @@ -289,6 +320,19 @@ impl<'a> Visit<'a> for FragmentVisitor<'_> { // Same rule as a request's own root, so a fragment's `to` and a // request's cannot be read differently. self.frag.target = crate::module::iq_target_from_to(&attrs); + let mut root = attrs.clone(); + crate::request::annotate_attr_arg_paths( + &mut root, + attrs_node, + self.aliases, + self.scope, + self.source, + Some(call.span().start as usize), + ); + self.frag.target_arg_path = root + .into_iter() + .find(|a| a.name == "to") + .and_then(|a| a.arg_path); } // The fragment's own children (e.g. `spam_list{spam_flow}`) — merged by // tag into a Request's children so cross-module attrs/children survive. @@ -318,9 +362,16 @@ impl<'a> Visit<'a> for FragmentVisitor<'_> { && method.contains("Mixin") && let Some(name) = callee_object(call).and_then(require_module_name) && name.starts_with("WASmaxOut") - && !self.frag.merged_callees.contains(&name) { - self.frag.merged_callees.push(name); + let arg = crate::request::MergeArg::of_in_mixin( + call, + self.scope, + self.source, + Some(call.span().start as usize), + ); + self.frag + .merged_callees + .push(MergeCallee { module: name, arg }); } walk::walk_call_expression(self, call); @@ -334,11 +385,25 @@ impl<'a> Visit<'a> for FragmentVisitor<'_> { /// for that field so the caller's guard discards the stanza rather than guess. pub(crate) fn resolve( index: &MixinIndex, - mixin_modules: &[String], -) -> (Option, Option, Option) { - // Transitive closure over merged_callees (BFS; visited set bounds cycles). - let mut visited = std::collections::HashSet::new(); - let mut queue: std::collections::VecDeque = mixin_modules.iter().cloned().collect(); + mixin_modules: &[MergeCallee], +) -> ( + Option, + Option, + Option, + Option, +) { + // Transitive closure over merged_callees (BFS; visited set bounds cycles). An xmlns is + // a property of the mixin itself and needs no prefix; the addressee's PATH is a + // property of what the mixin was handed, so the merge arguments ride along for it. + // Module → the frame it was reached through, as a list because a frame carries a path + // and the walk is a handful of mixins deep. + let mut reached: Vec<(String, MergeArg)> = Vec::new(); + let mut queue: std::collections::VecDeque<(String, MergeArg)> = mixin_modules + .iter() + .map(|c| (c.module.clone(), c.arg.clone())) + .collect(); + let mut target_path: Option = None; + let mut target_path_conflict = false; let mut xmlns: Option = None; let mut xmlns_conflict = false; let mut iq_type: Option = None; @@ -346,13 +411,47 @@ pub(crate) fn resolve( let mut target: Option = None; let mut target_conflict = false; - while let Some(name) = queue.pop_front() { - if !visited.insert(name.clone()) { - continue; + while let Some((name, acc)) = queue.pop_front() { + // A module is remembered WITH the frame it was reached through, because the same + // mixin reached twice through different arguments is one xmlns but two addresses + // — skipping the second reach by name alone would let the first one's path stand + // for both. Reaching it again through a different frame is a disagreement, and the + // answer is the same as everywhere else: no address. That also bounds the walk, + // which keying on the pair alone would not: a cycle carrying prefixes + // (`A → B/x → A/x/y → …`) never repeats a pair, while `Unnameable` absorbs and + // settles in one more round. + let acc = match reached.iter().find(|(m, _)| *m == name) { + Some((_, prev)) if *prev == acc => continue, + Some(_) => MergeArg::Unnameable, + None => acc, + }; + match reached.iter_mut().find(|(m, _)| *m == name) { + Some(slot) => { + if slot.1 == MergeArg::Unnameable { + continue; + } + slot.1 = MergeArg::Unnameable; + } + None => reached.push((name.clone(), acc.clone())), } let Some(frag) = index.get(&name) else { continue; }; + if let Some(p) = &frag.target_arg_path { + // Prefixed by what this mixin was handed, and dropped when the chain cannot + // name it — the same three outcomes its contribution gets. + let mut node = [WapChildNode { + arg_path: Some(p.clone()), + ..Default::default() + }]; + acc.apply(&mut node); + match (&target_path, node[0].arg_path.take()) { + (_, None) => target_path_conflict = true, + (Some(prev), Some(now)) if *prev != now => target_path_conflict = true, + (Some(_), Some(_)) => {} + (None, Some(now)) => target_path = Some(now), + } + } if let Some(x) = &frag.xmlns { match &xmlns { None => xmlns = Some(x.clone()), @@ -382,9 +481,7 @@ pub(crate) fn resolve( } } for c in &frag.merged_callees { - if !visited.contains(c) { - queue.push_back(c.clone()); - } + queue.push_back((c.module.clone(), acc.then(&c.arg))); } } @@ -399,33 +496,78 @@ pub(crate) fn resolve( } else { target }, + // An address the chain disagrees about, or cannot spell, is no address — and a + // disagreement about the addressee's KIND is one about its address too. A router + // whose arms address a group's own JID and the group server has one arm with a key + // and one without; publishing the first alongside `Unknown` would advertise one + // branch's argument as the request's answer. + if target_path_conflict || target_conflict { + None + } else { + target_path + }, ) } /// The transitive union of the `` children every referenced mixin contributes /// (following `merged_callees`), pre-merged by tag. The scanner merges this into a /// Request's children to recover cross-module attrs/children (e.g. `spam_flow`). +/// +/// Each hop carries the argument the previous frame handed it, composed along the way, +/// so a contribution reached through `mergeGroup(dst, e.groupArgs)` arrives with its +/// paths rooted where the REQUEST would read them rather than where the mixin does. A +/// hop no frame can name ([`MergeArg::Unnameable`] — typically a mixin dispatching on a +/// key of its own second parameter) drops the paths of everything below it: a +/// mixin-relative path published as a request address is a wrong address, and an absent +/// one is counted. +/// +/// A module reached twice keeps the first chain's prefix, as it keeps the first chain's +/// contribution — the merge is by tag and only ever adds. pub(crate) fn resolve_fragment_children( index: &MixinIndex, - mixin_modules: &[String], + mixin_modules: &[MergeCallee], ) -> Vec { - let mut visited = std::collections::HashSet::new(); - let mut queue: std::collections::VecDeque = mixin_modules.iter().cloned().collect(); - let mut out: Vec = Vec::new(); - while let Some(name) = queue.pop_front() { - if !visited.insert(name.clone()) { - continue; - } + // Two passes, because a module can be reached more than once — merged twice by one + // frame, or by two frames in a diamond — and the prefixes may disagree. The + // contribution is merged ONCE, so it can carry only one address; when the reaches + // disagree there is no single answer and the fold takes none. + let mut order: Vec = Vec::new(); + let mut reach: std::collections::HashMap = std::collections::HashMap::new(); + let mut queue: std::collections::VecDeque<(String, MergeArg)> = mixin_modules + .iter() + .map(|c| (c.module.clone(), c.arg.clone())) + .collect(); + while let Some((name, acc)) = queue.pop_front() { + let next = match reach.get(&name) { + // The same route again — nothing to revise, and nothing below it either. + Some(prev) if *prev == acc => continue, + // Reached before, by a route naming something else. Same disagreement rule + // as everywhere else here: two addresses for one thing is no address at all. + // Re-expanded so what hangs below it becomes unnameable too; `Unnameable` + // absorbs, so that settles in one more round rather than looping. + Some(_) => MergeArg::Unnameable, + None => { + order.push(name.clone()); + acc + } + }; + reach.insert(name.clone(), next.clone()); let Some(frag) = index.get(&name) else { continue; }; - merge_children(&mut out, &frag.children); for c in &frag.merged_callees { - if !visited.contains(c) { - queue.push_back(c.clone()); - } + queue.push_back((c.module.clone(), next.then(&c.arg))); } } + let mut out: Vec = Vec::new(); + for name in &order { + let Some(frag) = index.get(name) else { + continue; + }; + let mut contributed = frag.children.clone(); + reach[name].apply(&mut contributed); + merge_children(&mut out, &contributed); + } out } @@ -501,6 +643,7 @@ pub(crate) fn merge_children(into: &mut Vec, from: &[WapChildNode] // A fragment that marks the child repeated promotes it (the local build // may have seen a single template). existing.repeats = existing.repeats || fc.repeats; + crate::request::adopt_builder_facts(existing, fc); // Union variant groups (a node can carry several disjunctions); dedup so // a re-merge of the same fragment doesn't duplicate a group. for g in &fc.variant_groups { @@ -527,6 +670,7 @@ mod tests { value: None, required: false, enum_ref: None, + arg_path: None, } } @@ -538,6 +682,7 @@ mod tests { content: None, repeats: false, variant_groups: Vec::new(), + ..Default::default() } } @@ -546,7 +691,8 @@ mod tests { xmlns: xmlns.map(String::from), iq_type: ty, target: None, - merged_callees: callees.iter().map(|s| s.to_string()).collect(), + target_arg_path: None, + merged_callees: callees.iter().map(|s| MergeCallee::from(*s)).collect(), children: Vec::new(), } } @@ -581,7 +727,7 @@ mod tests { (IqTarget::Server, IqTarget::Unknown), ] { let idx = index(&[("A", with_target(first)), ("B", with_target(second))]); - let (_, _, got) = resolve(&idx, &["A".into(), "B".into()]); + let (_, _, got, _) = resolve(&idx, &["A".into(), "B".into()]); assert_eq!( got, Some(IqTarget::Unknown), @@ -616,7 +762,7 @@ mod tests { ("Xmlns", frag(Some("spam"), None, &[])), ("Type", frag(None, Some(IqType::Set), &[])), ]); - let (x, t, _) = resolve(&idx, &["Xmlns".into(), "Type".into()]); + let (x, t, _, _) = resolve(&idx, &["Xmlns".into(), "Type".into()]); assert_eq!(x.as_deref(), Some("spam")); assert_eq!(t, Some(IqType::Set)); } @@ -628,7 +774,7 @@ mod tests { ("Hack", frag(Some("w:biz"), None, &["Base"])), ("Base", frag(None, Some(IqType::Get), &[])), ]); - let (x, t, _) = resolve(&idx, &["Hack".into()]); + let (x, t, _, _) = resolve(&idx, &["Hack".into()]); assert_eq!(x.as_deref(), Some("w:biz")); assert_eq!(t, Some(IqType::Get), "type recovered transitively"); } @@ -639,7 +785,7 @@ mod tests { ("A", frag(Some("spam"), Some(IqType::Set), &[])), ("B", frag(Some("blocklist"), None, &[])), ]); - let (x, t, _) = resolve(&idx, &["A".into(), "B".into()]); + let (x, t, _, _) = resolve(&idx, &["A".into(), "B".into()]); assert_eq!(x, None, "ambiguous xmlns → discard, never guess"); assert_eq!(t, Some(IqType::Set)); } @@ -650,7 +796,7 @@ mod tests { ("A", frag(Some("x"), None, &["B"])), ("B", frag(None, Some(IqType::Get), &["A"])), ]); - let (x, t, _) = resolve(&idx, &["A".into()]); + let (x, t, _, _) = resolve(&idx, &["A".into()]); assert_eq!(x.as_deref(), Some("x")); assert_eq!(t, Some(IqType::Get)); } @@ -758,6 +904,110 @@ mod tests { assert_eq!(names, vec!["jid", "spam_flow"]); } + #[test] + fn merge_children_carries_the_builder_facts_of_a_folded_fragment() { + // `mergeStanzas` folds a fragment onto a destination with the same tag, and the + // merge used to union attrs and children and drop everything else — so a mixin's + // `` arrived at the request as a bare tag, and a cardinality resolved at + // the fragment's own call site was lost with it. + // + // Unexercised by the current bundle (every mixin that carries these builds a tag + // the destination does not already have, so it is appended whole), which is + // exactly why it is asserted directly: the loss would be silent and would look + // like an extraction gap rather than a merge that discards. + let mut into = vec![node("subject", &[], vec![])]; + let mut frag = node("subject", &[], vec![]); + frag.content = Some(wa_ir::WapContent { + kind: wa_ir::WapContentKind::Dynamic, + arg_path: Some(vec![wa_ir::WapArgSegment { + key: "subjectElementValue".to_string(), + list: false, + }]), + ..Default::default() + }); + frag.arg_path = Some(vec![wa_ir::WapArgSegment { + key: "subjectArgs".to_string(), + list: false, + }]); + merge_children(&mut into, &[frag]); + assert_eq!(into.len(), 1, "same tag merges, not duplicates"); + let merged = &into[0]; + assert_eq!( + merged + .content + .as_ref() + .and_then(|c| c.arg_path.as_ref()) + .map(|p| p[0].key.as_str()), + Some("subjectElementValue"), + "the element value survives the fold" + ); + assert!( + merged.arg_path.is_none(), + "but the fragment's own argument address does not transfer: {:?}", + merged.arg_path + ); + } + + #[test] + fn merge_children_does_not_hand_a_fragments_address_to_the_destination() { + // Content and bounds are wire facts about the element — the merged stanza really + // does carry that value, really is bounded that way. An argument path is not: it + // addresses the object a node is built FROM, and a node two call sites build + // together has no single one. Adopting the fragment's would tell a consumer that + // handing over `pictureArgs` produces the whole ``, when half of it is + // built at the destination's own site from something else. + let mut into = vec![node("picture", &[], vec![])]; + let mut frag = node("picture", &[], vec![]); + frag.arg_path = Some(vec![wa_ir::WapArgSegment { + key: "pictureArgs".to_string(), + list: false, + }]); + merge_children(&mut into, &[frag]); + assert!(into[0].arg_path.is_none(), "{:?}", into[0].arg_path); + } + + #[test] + fn merge_children_does_not_overwrite_the_destinations_own_facts() { + // Fill-if-empty, not overwrite: the destination is the node built at the call + // site and is the more specific description. A fragment only ever ADDS. + let mut into = vec![node("body", &[], vec![])]; + into[0].repeat_max = Some(10); + let mut frag = node("body", &[], vec![]); + frag.repeat_max = Some(99); + merge_children(&mut into, &[frag]); + assert_eq!(into[0].repeat_max, Some(10)); + } + + #[test] + fn merge_children_keeps_an_explicitly_unbounded_maximum_open() { + // A minimum with no maximum is the contract's *stated* unbounded range (WA's + // `1/0`), not a half-filled pair. Taking a fragment's finite ceiling would cap a + // list the destination's own call site left open, so bounds move together or not + // at all. + let mut into = vec![node("participant", &[], vec![])]; + into[0].repeats = true; + into[0].repeat_min = Some(1); + let mut frag = node("participant", &[], vec![]); + frag.repeats = true; + frag.repeat_min = Some(0); + frag.repeat_max = Some(10); + merge_children(&mut into, &[frag]); + assert_eq!((into[0].repeat_min, into[0].repeat_max), (Some(1), None)); + } + + #[test] + fn merge_children_never_weakens_a_required_destination() { + // `Required` is a real state AND the serde default, so a fill-if-empty test on + // `presence` cannot tell "unconditional" from "unclassified". A child that some + // call site emits unconditionally is unconditional, whatever an optional + // fragment for the same tag says. + let mut into = vec![node("locked", &[], vec![])]; + let mut frag = node("locked", &[], vec![]); + frag.presence = wa_ir::WapChildPresence::Optional; + merge_children(&mut into, &[frag]); + assert_eq!(into[0].presence, wa_ir::WapChildPresence::Required); + } + #[test] fn merge_children_appends_non_matching_tag() { let mut into = vec![node("a", &[], vec![])]; @@ -799,4 +1049,167 @@ mod tests { "transitive child attr via callee" ); } + + /// A `MergeCallee` reached through a named key of the caller's argument object. + fn keyed(module: &str, key: &str) -> MergeCallee { + MergeCallee { + module: module.to_string(), + arg: MergeArg::Prefix(vec![wa_ir::WapArgSegment { + key: key.to_string(), + list: false, + }]), + } + } + + fn attr_path(n: &WapChildNode, name: &str) -> Vec { + n.attrs + .iter() + .find(|a| a.name == name) + .and_then(|a| a.arg_path.as_ref()) + .map(|p| p.iter().map(|s| s.key.clone()).collect()) + .unwrap_or_default() + } + + #[test] + fn a_cycle_carrying_prefixes_still_terminates() { + // Two mixins that fold each other, each through a key of its own arguments. Every + // trip round the loop composes a longer prefix, so no (module, frame) pair ever + // repeats — remembering the module and collapsing a second, different frame to + // `Unnameable` is what ends the walk. Without it this hangs rather than fails. + let mut a = frag(Some("w:g2"), Some(IqType::Get), &[]); + a.merged_callees = vec![keyed("B", "x")]; + let mut b = frag(None, None, &[]); + b.merged_callees = vec![keyed("A", "y")]; + b.target = Some(IqTarget::GroupJid); + b.target_arg_path = Some(vec![wa_ir::WapArgSegment { + key: "iqTo".to_string(), + list: false, + }]); + let idx = index(&[("A", a), ("B", b)]); + + let (xmlns, ty, target, path) = resolve(&idx, &[keyed("A", "start")]); + assert_eq!(xmlns.as_deref(), Some("w:g2")); + assert_eq!(ty, Some(IqType::Get)); + assert_eq!(target, Some(IqTarget::GroupJid)); + // And no address: going round the loop reaches the addressee mixin through a + // second, different frame, which is the disagreement rule doing its job. A cyclic + // merge graph is not a shape WA can execute anyway — it would recurse forever — + // so the honest answer to "where does its `to` come from" is nothing. + assert!(path.is_none(), "{path:?}"); + } + + #[test] + fn two_reaches_of_one_addressee_mixin_disagree_to_nothing() { + // The same mixin merged twice through different arguments is one xmlns but TWO + // addresses. Deduplicating the walk by module name alone would let the first + // reach's path stand for both callers, which is a wrong address rather than a + // missing one. + let mut m = frag(Some("w:g2"), Some(IqType::Get), &[]); + m.target = Some(IqTarget::GroupJid); + m.target_arg_path = Some(vec![wa_ir::WapArgSegment { + key: "iqTo".to_string(), + list: false, + }]); + let idx = index(&[("Addr", m)]); + + let (_, _, target, path) = resolve(&idx, &[keyed("Addr", "left"), keyed("Addr", "right")]); + assert_eq!( + target, + Some(IqTarget::GroupJid), + "still one kind of addressee" + ); + assert!(path.is_none(), "but no single address: {path:?}"); + + // Agreeing reaches are not a disagreement. + let (_, _, _, path) = resolve(&idx, &[keyed("Addr", "left"), keyed("Addr", "left")]); + assert_eq!( + path.map(|p| p.iter().map(|s| s.key.clone()).collect::>()), + Some(vec!["left".to_string(), "iqTo".to_string()]) + ); + } + + #[test] + fn a_fragment_chain_composes_the_arguments_it_was_handed() { + // The mixin closure walks by MODULE NAME and holds no call site, which is why a + // contribution used to arrive with paths relative to whichever mixin resolved + // them: `configMixinsArgs → configPlatform` published at a request root whose + // real address starts two hops earlier. Each hop now carries the argument it was + // handed, and the prefixes compose down the chain. + let mut inner = frag(Some("push"), Some(IqType::Set), &[]); + let mut cfg = node("config", &["platform"], vec![]); + cfg.attrs[0].arg_path = Some(vec![wa_ir::WapArgSegment { + key: "configPlatform".to_string(), + list: false, + }]); + inner.children = vec![cfg]; + let mut group = frag(None, None, &[]); + group.merged_callees = vec![keyed("Inner", "setConfig")]; + let idx = index(&[("Inner", inner), ("Group", group)]); + + let kids = resolve_fragment_children(&idx, &[keyed("Group", "groupArgs")]); + assert_eq!( + attr_path(&kids[0], "platform"), + vec!["groupArgs", "setConfig", "configPlatform"], + "the request's address, not the innermost mixin's" + ); + } + + #[test] + fn two_call_sites_that_disagree_leave_no_address() { + // One contribution, merged once, cannot carry two addresses. A mixin merged twice + // with different argument objects — or reached by two routes of a diamond — has + // no single prefix, and keeping the first would publish it for the second's + // callers as well. + let mut inner = frag(Some("push"), Some(IqType::Set), &[]); + let mut cfg = node("config", &["platform"], vec![]); + cfg.attrs[0].arg_path = Some(vec![wa_ir::WapArgSegment { + key: "configPlatform".to_string(), + list: false, + }]); + inner.children = vec![cfg]; + let idx = index(&[("Inner", inner)]); + + let kids = + resolve_fragment_children(&idx, &[keyed("Inner", "left"), keyed("Inner", "right")]); + assert!( + attr_path(&kids[0], "platform").is_empty(), + "{:?}", + kids[0].attrs + ); + // And agreeing routes are not a disagreement. + let kids = + resolve_fragment_children(&idx, &[keyed("Inner", "left"), keyed("Inner", "left")]); + assert_eq!( + attr_path(&kids[0], "platform"), + vec!["left", "configPlatform"] + ); + } + + #[test] + fn a_hop_no_frame_can_name_drops_the_paths_below_it() { + // The other outcome: a merge whose argument this chain cannot spell. A relative + // path published as an absolute one is a wrong address, which is worse than an + // absent one — the absent one is counted. + let mut inner = frag(Some("push"), Some(IqType::Set), &[]); + let mut cfg = node("config", &["platform"], vec![]); + cfg.attrs[0].arg_path = Some(vec![wa_ir::WapArgSegment { + key: "configPlatform".to_string(), + list: false, + }]); + inner.children = vec![cfg]; + let idx = index(&[("Inner", inner)]); + + let kids = resolve_fragment_children( + &idx, + &[MergeCallee { + module: "Inner".to_string(), + arg: MergeArg::Unnameable, + }], + ); + assert!( + attr_path(&kids[0], "platform").is_empty(), + "{:?}", + kids[0].attrs + ); + } } diff --git a/crates/wa-scan/src/module.rs b/crates/wa-scan/src/module.rs index 0f4be88..1005c44 100644 --- a/crates/wa-scan/src/module.rs +++ b/crates/wa-scan/src/module.rs @@ -15,7 +15,7 @@ use crate::alias::{AliasMap, build_alias_map}; use crate::attrs::{extract_attrs_from_obj, parse_wap_call}; use crate::helper_index::HelperIndex; use crate::mixin_index::MixinIndex; -use crate::request::{VarScope, build_var_scope, resolve_child_node}; +use crate::request::{VarScope, build_var_scope, enforce_argument_boundary, resolve_child_node}; use crate::response_index::ResponseIndex; use wa_oxc::{arg_expr, as_call, callee_method, callee_object}; @@ -152,7 +152,7 @@ pub fn scan_module_outcome( let mixin_resolved = if needs_resolution { crate::mixin_index::resolve(mixins, &scanner.mixin_callees) } else { - (None, None, None) + (None, None, None, None) }; // The union names the fragments the MODULE folds in, not the ones this call does. // With one `` builder in the module those are the same statement; with more than @@ -161,13 +161,27 @@ pub fn scan_module_outcome( // withholding is remembered, because "some fragment in this module names an addressee // and we cannot say whose" is `Unknown`, not `Unset`. let one_builder = scanner.iq_calls.len() == 1; + // Same attribution rule as the addressee itself: with more than one builder the + // module's mixin set is not this call's, so its address is not either. + let mixin_target_path = if one_builder { + mixin_resolved.3.clone() + } else { + None + }; let mixin_target = if one_builder { mixin_resolved.2 } else { None }; let target_unattributable = !one_builder && mixin_resolved.2.is_some(); // Cross-module `mergeStanzas` fragments: the children/attrs the referenced // mixins add to the `` (e.g. `spam_list{spam_flow}`). Merged by tag into the // locally-built children so those cross-module fields aren't lost. + // + // Held to the same rule as the addressee above, and for the same reason: the callee + // list is the MODULE's, so with more than one builder a sibling's `` — and + // now its argument addresses — would be merged into a request that folds no mixin at + // all. No module in the current bundle has both, so this withholds nothing today; it + // is here so the shape cannot start leaking silently. Attributing each merge call to + // the builder that encloses it is the real answer, and a larger change than this. let referenced_mixins = !scanner.mixin_callees.is_empty(); - let frag_children = if scanner.mixin_callees.is_empty() { + let frag_children = if scanner.mixin_callees.is_empty() || !one_builder { Vec::new() } else { crate::mixin_index::resolve_fragment_children(mixins, &scanner.mixin_callees) @@ -218,10 +232,27 @@ pub fn scan_module_outcome( crate::mixin_index::count_recovered_fields(&iq.children, &frag_children); fields_recovered = fields_recovered.max(recovered); crate::mixin_index::merge_children(&mut iq.children, &frag_children); + // A contribution can arrive holding the empty path — a mixin whose + // own parameter is a payload, handed the request's whole argument + // object. It composed to nothing, and nothing is not an address. + crate::request::drop_empty_arg_paths(&mut iq.children); Some(ResolvedIqCall { namespace, iq_type, target: iq.target.unwrap_or(IqTarget::Unset), + // The builder's own `to` wins whole: if it writes one, its + // address is the answer even when that address is nothing (a + // constant addressee, or one this scan could not read). Falling + // back on the absence of a PATH rather than the absence of a `to` + // would hand such a request a mixin's argument key for an + // addressee that argument does not control. `iq.target` is `None` + // exactly when the builder writes no `to`, which is the state the + // fallback is for — and where most runtime addressees live. + target_arg_path: if iq.wrote_to { + iq.target_arg_path.clone() + } else { + mixin_target_path.clone() + }, children: iq.children, export: iq.export, }) @@ -240,6 +271,10 @@ pub fn scan_module_outcome( d.namespace == r.namespace && d.iq_type == r.iq_type && d.target == r.target + // Two builders can write the same stanza and read the addressee from + // different keys; collapsing them would hand one builder's contract to + // the other's callers. + && d.target_arg_path == r.target_arg_path && d.children == r.children }) { deduped.push(r); @@ -335,6 +370,7 @@ pub fn scan_module_outcome( namespace: iq.namespace, iq_type: iq.iq_type, target: iq.target, + target_arg_path: iq.target_arg_path, children: iq.children, }, response: response.clone(), @@ -375,6 +411,16 @@ fn ends_ci(s: &str, suffix: &str) -> bool { struct IqCall { namespace: Option, iq_type: Option, + /// The argument path of the root `to`, when the builder reads its addressee from an + /// argument. Resolved here because `to` is consumed into [`IqCall::target`] and the + /// attribute itself never reaches the IR. + target_arg_path: Option, + /// Whether the builder writes a root `to` at all — asked separately from + /// [`IqCall::target`], which a folded-in mixin's addressee overwrites before the + /// stanza is assembled. A builder that writes its own `to` owns the answer whole, + /// including when that answer is "no address to supply": inheriting a mixin's key + /// there would name an argument that does not control this request's addressee. + wrote_to: bool, /// `None` when the builder writes no `to` — distinct from a `to` that resolved to /// nothing, which is [`IqTarget::Unknown`] and must not be overwritten by a mixin. target: Option, @@ -390,6 +436,7 @@ struct ResolvedIqCall { namespace: String, iq_type: IqType, target: IqTarget, + target_arg_path: Option, children: Vec, export: Option, } @@ -406,9 +453,10 @@ struct ModuleScanner<'src> { /// Populated when the wrapper is visited (parent-before-child), read when the /// inner iq call is visited. type_hints: std::collections::HashMap, - /// `WASmaxOut…` mixin modules this module folds in (by name, source order, - /// deduped) — the candidates for cross-module xmlns/type resolution. - mixin_callees: Vec, + /// `WASmaxOut…` mixin modules this module folds in (source order, deduped), each + /// with the argument it was handed — the candidates for cross-module xmlns/type + /// resolution, and the prefixes their contributions need. + mixin_callees: Vec, /// The export name currently being walked (`e. = …`), so an iq call is /// tagged with the function that lexically encloses it. Saved/restored around /// each member-assignment to handle nesting. @@ -449,15 +497,27 @@ impl<'a> Visit<'a> for ModuleScanner<'_> { } // Collect `o("WASmaxOut…").merge…Mixin(…)` references so the post-walk - // pass can resolve xmlns/type from those mixins' fragments. + // pass can resolve xmlns/type from those mixins' fragments. EVERY call is kept, + // including a second one to the same module: the two may hand it different + // argument objects, and which prefix a contribution needs is decided where the + // chain is walked, not here. if let Some(method) = callee_method(call) && method.starts_with("merge") && method.contains("Mixin") && let Some(name) = callee_object(call).and_then(require_module_name) && name.starts_with("WASmaxOut") - && !self.mixin_callees.contains(&name) { - self.mixin_callees.push(name); + // The argument this request hands the mixin is the prefix its contribution + // needs; recorded here because the fold happens in a later pass that walks + // mixins by module name and never sees this call. + let arg = crate::request::MergeArg::of( + call, + self.scope, + self.source, + Some(call.span().start as usize), + ); + self.mixin_callees + .push(crate::mixin_index::MergeCallee { module: name, arg }); } let hint = self.type_hints.get(&call.span().start).copied(); @@ -560,6 +620,26 @@ impl ModuleScanner<'_> { None => mixin_type, }; let target = iq_target_from_to(&attrs); + // The root attributes are consumed into namespace/type/target and never emitted, + // so the `to` attribute's address has to be read here or not at all. A consumer + // running the vendor builder needs it for exactly the requests whose addressee is + // supplied at runtime — a group's own JID, a newsletter's — which is the state the + // IR now names rather than flattening into the server. + let target_arg_path = wap + .attrs_node + .and_then(|n| { + let mut root = attrs.clone(); + crate::request::annotate_attr_arg_paths( + &mut root, + n, + self.aliases, + self.scope, + self.source, + Some(call.span().start as usize), + ); + root.into_iter().find(|a| a.name == "to") + }) + .and_then(|a| a.arg_path); let mut children = Vec::new(); for child_arg in wap.child_args { @@ -582,11 +662,17 @@ impl ModuleScanner<'_> { )); } } + // A builder with no single options object publishes no paths at all — including + // any that arrived inlined from a helper or a mapper. Once for the whole call: + // every child of one `smax("iq", …)` shares the frame that built it. + enforce_argument_boundary(&mut children, self.scope, call.span().start as usize); Some(IqCall { namespace, iq_type, target, + target_arg_path, + wrote_to: attrs.iter().any(|a| a.name == "to"), children, export: self.current_export.clone(), }) @@ -793,6 +879,33 @@ mod tests { } } + #[test] + fn a_sibling_builders_mixin_does_not_furnish_this_one() { + // The same rule as the addressee above, for what the fragment BUILDS. The callee + // list is the module's, so a `` — and the argument addresses that now + // ride with it — would otherwise be merged into a builder that folds no mixin. + let mixin = r#"__d("WASmaxOutBazIQGetRequestMixin",["WAWap","WASmaxJsx"],function(g,r,d,o,e,i){ + e.mergeBazIQGetRequestMixin = function(s){ return o("WASmaxJsx").smax("iq", { xmlns: "w:baz", type: "get" }, o("WASmaxJsx").smax("spam_list", { flow: o("WAWap").CUSTOM_STRING("x") })); }; + });"#; + let m = r#"__d("WASmaxOutBazRequest",["WASmaxJsx","WASmaxOutBazIQGetRequestMixin"],function(g,r,d,o,e,i){ + e.makeA = function(){ var q = o("WASmaxJsx").smax("iq", { xmlns: "w:baz", type: "get" }); o("WASmaxOutBazIQGetRequestMixin").mergeBazIQGetRequestMixin(q); return q; }; + e.makeB = function(){ return o("WASmaxJsx").smax("iq", { xmlns: "w:other", type: "set" }); }; + });"#; + let bundle = format!("{mixin}\n{m}"); + let defs = wa_transform::extract_module_definitions(&bundle); + let mixins = crate::mixin_index::build_pass(&defs, &bundle, &hi()); + let s = scan_module_source(m, &mixins, &ri(), &hi()); + assert_eq!(s.len(), 2); + for st in &s { + assert!( + st.request.children.is_empty(), + "{}: a module-scoped fragment is not this call's: {:?}", + st.namespace, + st.request.children + ); + } + } + #[test] fn a_builder_that_writes_no_to_reports_unset() { // Distinct from `Unknown`: nothing addresses this stanza, so an emitter should diff --git a/crates/wa-scan/src/request.rs b/crates/wa-scan/src/request.rs index 0ce245d..72ae73f 100644 --- a/crates/wa-scan/src/request.rs +++ b/crates/wa-scan/src/request.rs @@ -18,13 +18,16 @@ use std::collections::{BTreeMap, HashMap}; use oxc_allocator::Allocator; use oxc_ast::ast::{ - Argument, ArrowFunctionExpression, AssignmentExpression, Expression, Function, Program, - Statement, VariableDeclaration, + Argument, ArrowFunctionExpression, AssignmentExpression, Expression, Function, + ObjectPropertyKind, Program, PropertyKey, Statement, VariableDeclaration, }; use oxc_ast_visit::{Visit, walk}; use oxc_span::GetSpan; use oxc_syntax::scope::ScopeFlags; -use wa_ir::{WapChildNode, WapContent, WapContentKind}; +use wa_ir::{ + WapArgPath, WapArgSegment, WapAttrDef, WapAttrKind, WapChildNode, WapChildPresence, WapContent, + WapContentKind, +}; use crate::alias::{AliasMap, build_alias_map, resolve_owner}; use crate::attrs::{extract_attrs_from_obj, parse_wap_call}; @@ -45,13 +48,54 @@ const BIG_ENDIAN_CONTENT: &str = "BIG_ENDIAN_CONTENT"; /// Distinguishes a value payload from a child node structurally: a string literal /// is a fixed `Const`; `BIG_ENDIAN_CONTENT(x, n)` is `n` bytes; a member value /// reference (`e.keyPair.pubKey`, `e.signature`) is opaque `Dynamic` bytes/text. -/// A bare identifier is deliberately ignored — it may be a node variable resolved -/// elsewhere, not content. -fn leaf_content(child_args: &[Argument]) -> Option { +/// +/// A bare identifier is content only when it resolves to an argument path. That +/// discriminates the two things a minified identifier can be in this position: WA's +/// builders bind the payload to a local first (`var t = e.subjectElementValue; +/// smax("subject", null, t)` — ``'s whole payload), while a node variable +/// (`var n = smax("body", …); smax("description", null, n)`) is a child and roots at no +/// parameter. Ignoring identifiers outright was the safe half of that distinction and +/// dropped the payload of every request whose builder destructures first — which is +/// every builder that lives behind a mixin. +fn leaf_content( + child_args: &[Argument], + scope: &VarScope, + module_source: &str, + ref_off: Option, + aliases: &AliasMap, +) -> Option { if child_args.len() != 1 { return None; } - content_of_expr(arg_expr(child_args.first()?)?) + let e = arg_expr(child_args.first()?)?; + // A node is a CHILD, never a payload. The identifier rule below tells a local bound to + // a `smax(…)` call from one bound to a value; this is the same question asked of the + // expression written in place. It matters most where children are collected flat — a + // `.map()` callback returning `wap("product", null, wap("id", null, v))` — because + // there the wrapper's child argument is still a node while the walk below would + // descend into it, find the element parameter and report the inner node's payload as + // the wrapper's own. + if as_call(e).is_some_and(|call| parse_wap_call(call, aliases).is_some()) { + return None; + } + // The EMPTY path is kept here, unlike everywhere else it is a no-op: a template whose + // whole parameter is the payload (`function t(v){ return wap("id", null, v) }`, over a + // list of scalars) says its content is the element itself, and the combinator that + // knows the list rebases that empty suffix onto `productIds[]`. Filtering it out made + // the node contentless before anything could. What survives unprefixed to a request + // root is dropped by [`drop_empty_arg_paths`] — the argument object itself is not an + // address a consumer supplies. + let path = relative_arg_path(e, scope, module_source, ref_off, 0); + let mut content = content_of_expr(e).or_else(|| { + path.is_some().then_some(WapContent { + kind: WapContentKind::Dynamic, + ..Default::default() + }) + })?; + if content.arg_path.is_none() { + content.arg_path = path; + } + Some(content) } fn content_of_expr(e: &Expression) -> Option { @@ -110,6 +154,105 @@ const WILDCARD_TAG: &str = "smax$any"; /// resolution purely structural — identical to the pre-Phase-3 behavior. pub(crate) type MixinContributions = BTreeMap>; +/// How a `merge…Mixin(dst, args)` call handed its argument object to the mixin it folds +/// in — the prefix that mixin's own paths need to become absolute in the calling frame. +/// +/// A mixin resolves its paths against whatever it was handed, and only the merge site +/// knows what that was. This is the same three-outcome rule the combinator call sites +/// use ([`rebase_template`]), recorded rather than applied, because the merge that names +/// the argument and the fold that needs the prefix happen in different passes: the +/// [`crate::mixin_index`] walks a mixin chain by MODULE NAME, with no call site in hand. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum MergeArg { + /// `merge…(dst, e.someArgs)` — the callee's paths hang off this path. + Prefix(WapArgPath), + /// `merge…(dst, e)` — the callee was handed this frame's whole argument object, so + /// its paths are already absolute and need no prefix. An empty prefix is a result, + /// not a failure. + Whole, + /// The frame cannot name what it passed: a key off a parameter it has no argument + /// root for (a mixin's own `merge(dst, args)` frame), an object literal, or no + /// argument at all. The callee's paths address something no one in this chain can + /// spell, so they are dropped rather than published as request addresses. + Unnameable, +} + +impl MergeArg { + /// The merge argument of a `merge…Mixin(dst, args)` call made by a REQUEST builder, + /// whose frame is a single options object. + pub(crate) fn of( + call: &oxc_ast::ast::CallExpression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + ) -> MergeArg { + Self::read(call, scope, module_source, ref_off, RootRule::SoleParameter) + } + + /// The same, for a merge call made INSIDE a mixin, whose own frame is WA's + /// `merge(dst, args)` pair rather than a lone options object. + /// + /// Without this, a mixin that dispatches on a key of its own argument — + /// `mergeSetConfigMixin(dst, args.setConfig)`, the shape of every `…MixinGroup` — + /// names a path in a frame the builder rule says has no argument object, so the + /// whole subtree below it is dropped as unnameable. The rule is applied here and + /// nowhere else: a request builder with two parameters is the legacy `WAWeb*Job` + /// shape, which addresses nothing. + pub(crate) fn of_in_mixin( + call: &oxc_ast::ast::CallExpression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + ) -> MergeArg { + Self::read(call, scope, module_source, ref_off, RootRule::MergeFrame) + } + + fn read( + call: &oxc_ast::ast::CallExpression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + rule: RootRule, + ) -> MergeArg { + let Some(arg) = call.arguments.get(1).and_then(arg_expr) else { + // `merge…(dst)` with no argument object: the callee reads nothing this frame + // supplied, so any path it carries came from somewhere this frame cannot name. + return MergeArg::Unnameable; + }; + match relative_arg_path_with(arg, scope, module_source, ref_off, 0, rule) { + Some(p) if p.is_empty() => MergeArg::Whole, + Some(p) => MergeArg::Prefix(p), + None => MergeArg::Unnameable, + } + } + + /// This hop applied after `outer` — the composition of a chain of merges. + /// + /// `Unnameable` absorbs: once one hop cannot be spelled, nothing further down the + /// chain can be either. `Whole` is the identity, which is what makes the ordinary + /// case (a mixin handed the request's whole argument object) cost nothing. + pub(crate) fn then(&self, inner: &MergeArg) -> MergeArg { + match (self, inner) { + (MergeArg::Unnameable, _) | (_, MergeArg::Unnameable) => MergeArg::Unnameable, + (MergeArg::Whole, x) | (x, MergeArg::Whole) => x.clone(), + (MergeArg::Prefix(a), MergeArg::Prefix(b)) => { + let mut p = a.clone(); + p.extend(b.iter().cloned()); + MergeArg::Prefix(p) + } + } + } + + /// Rebase a mixin's contribution into the frame this argument was read in. + pub(crate) fn apply(&self, nodes: &mut [WapChildNode]) { + match self { + MergeArg::Prefix(p) => prefix_arg_paths(nodes, p), + MergeArg::Whole => {} + MergeArg::Unnameable => clear_arg_paths(nodes), + } + } +} + /// One initializer of a tracked variable, as byte spans into the module source. #[derive(Clone)] struct VarInit { @@ -127,16 +270,90 @@ struct VarInit { owner_fn: Option<(usize, usize)>, } +/// Which formal of a frame counts as its argument object. +/// +/// The default is the builder rule, and it is deliberately narrow: a smax builder or +/// template takes exactly one options object, so a second parameter means the function +/// has no single argument to address and publishes no paths. +#[derive(Clone, Copy, PartialEq, Eq)] +enum RootRule { + /// Exactly one parameter, and that parameter is the object. + SoleParameter, + /// WA's mixin signature `merge(dst, args)`: the destination stanza first, the + /// argument object second. Applied ONLY where the frame is known to be a mixin merge + /// — the mixin index reading what a merge call handed its callee — and never to the + /// generic resolution, where the same two-parameter shape is the legacy `WAWeb*Job` + /// builder that names nothing. + MergeFrame, +} + /// Variable/function name → all initializers seen (offset-based, lifetime-free). #[derive(Default)] pub(crate) struct VarScope { vars: HashMap>, + /// Every function as `(body_start, body_end, parameter_names)` — all of them, not + /// only the ones shaped like a builder. + /// + /// A smax builder/template takes exactly one argument object, so a lone parameter + /// *is* the argument root: every `var x = .` inside the body names a + /// path into it. Recorded here rather than derived from the body, because the + /// parameter list lives in the function header — outside the body span the resolver + /// re-parses. + /// + /// The zero- and many-parameter functions are recorded too, even though they have no + /// argument root, because their spans decide which declarations are lexically in + /// scope: a nested helper's locals sit inside its parent's byte range without being + /// in its scope, and a span list that skipped those functions could not tell the two + /// apart. + fn_params: Vec<(usize, usize, Vec)>, } impl VarScope { fn push(&mut self, name: &str, init: VarInit) { self.vars.entry(name.to_string()).or_default().push(init); } + + /// The innermost function whose body contains `off`, with its parameter names. + /// + /// The innermost wins: a template nested inside a builder shadows the builder's own + /// parameter, exactly as JS scoping does with the minifier's reused single letters. + fn innermost_fn(&self, off: usize) -> Option<(usize, usize, &Vec)> { + self.fn_params + .iter() + .filter(|(s, e, _)| *s <= off && off <= *e) + .min_by_key(|(s, e, _)| e.saturating_sub(*s)) + .map(|(s, e, params)| (*s, *e, params)) + } + + /// The argument-object parameter of the innermost function containing `off` — the + /// root identifier a member chain must start at for it to be an argument path. + /// `None` outside every tracked function, when the context is unknown, or when the + /// enclosing function does not take exactly one parameter: a marker template + /// (`function u(){ return smax("locked", null) }`) reads no arguments at all, and a + /// multi-parameter function has no single argument object to address. + fn arg_root_at(&self, off: usize) -> Option<(usize, usize, &str)> { + self.root_at(off, RootRule::SoleParameter) + } + + /// [`Self::arg_root_at`] under an explicit rule; see [`RootRule`]. + fn root_at(&self, off: usize, rule: RootRule) -> Option<(usize, usize, &str)> { + let (s, e, params) = self.innermost_fn(off)?; + match (rule, params.as_slice()) { + (_, [only]) => Some((s, e, only.as_str())), + (RootRule::MergeFrame, [_dst, args]) => Some((s, e, args.as_str())), + _ => None, + } + } + + /// Whether an initializer at `init_off` is written in the SAME function as `off`, + /// rather than merely inside its byte range. A nested helper's body lies within its + /// parent's span, so a range test alone lets `function h(e){ var x = e.inner }` count + /// as an initializer of the parent's own `x` — which, under the ambiguity rule, + /// silently drops the parent's valid path instead of merely picking the wrong one. + fn same_fn(&self, off: usize, init_off: usize) -> bool { + let span = |o| self.innermost_fn(o).map(|(s, e, _)| (s, e)); + span(off) == span(init_off) + } } /// Build a [`VarScope`] from a parsed program: every `var/let/const x = init` and @@ -206,6 +423,11 @@ impl<'a> Visit<'a> for ScopeBuilder { let pushed = fn_body_span(func); if let Some(span) = pushed { self.fn_stack.push(span); + // The parameter list lives in the header, outside the body span the + // resolver later re-parses in isolation, so it must be captured here. + self.scope + .fn_params + .push((span.0, span.1, param_names(&func.params))); } walk::walk_function(self, func, flags); if pushed.is_some() { @@ -216,6 +438,15 @@ impl<'a> Visit<'a> for ScopeBuilder { fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) { let span = (arrow.body.span.start as usize, arrow.body.span.end as usize); self.fn_stack.push(span); + // An arrow is the same argument-object shape as the `function` form above and + // gets the same treatment, so the two spellings cannot diverge. Today's bundle is + // down-levelled to ES5 and contains no arrow inside any `WASmax*` module, so this + // recovers nothing yet; recording it is what stops a build that stops + // down-levelling from dropping paths silently, with the loss visible only as a + // fall in the floor-guarded counts after the fact. + self.scope + .fn_params + .push((span.0, span.1, param_names(&arrow.params))); walk::walk_arrow_function_expression(self, arrow); self.fn_stack.pop(); } @@ -246,12 +477,573 @@ impl<'a> Visit<'a> for ScopeBuilder { } } +/// A function's parameter names, in order — the frame's received values. +/// +/// Exactly one of them is the structural definition of "has an argument object" +/// ([`VarScope::arg_root_at`]), and it is what keeps this honest across the two builder +/// families in the IQ domain. Every `WASmaxOut*Request` builder takes one options +/// object, so a path into it is a complete address. The legacy `WAWeb*Job` builders take +/// positional parameters (`function(e, t)`); a path there would name a key without +/// naming which parameter it hangs off, which reads like an address and is not one. +/// Those requests get no path and are counted instead. +/// +/// A rest parameter yields nothing at all: the arity is open, so no position is a fixed +/// argument object. A destructuring pattern is skipped for the same reason — it names no +/// single binding the resolver can root a chain at. +fn param_names(params: &oxc_ast::ast::FormalParameters) -> Vec { + if params.rest.is_some() { + return Vec::new(); + } + // Every formal must be a plain binding, or the list is abandoned entirely. Silently + // skipping a destructured one changes the apparent ARITY — `function f(e, {x})` + // would read as a single-argument builder and publish `e`'s keys as though they + // addressed the only options object. + let mut names = Vec::with_capacity(params.items.len()); + for p in ¶ms.items { + match p.pattern.get_identifier_name() { + Some(n) => names.push(n.to_string()), + None => return Vec::new(), + } + } + names +} + fn fn_body_span(f: &Function) -> Option<(usize, usize)> { f.body .as_ref() .map(|b| (b.span.start as usize, b.span.end as usize)) } +// ─── Argument paths (the builder side of the contract) ─────────────────────────── +// +// Every value a request carries is read off the single argument object WA's builder +// takes. The path is recovered from two structural facts and nothing else: a builder +// or template function's FIRST parameter is that argument object, and a `var x = +// .` inside its body binds `x` to ``. Names are never pattern-matched +// — `…Args`, `has…` and `any…` are WA conventions that make the IR readable, not +// evidence about where a value goes. +// +// Composition is by prefixing rather than by threading a context down the recursion: +// each function body resolves its paths relative to its OWN argument root, and the +// call site that supplies that root (a `REPEATED_CHILD` list, an `OPTIONAL_CHILD` +// argument object, a `merge…Mixin` argument) prefixes the subtree it just resolved. +// A nested combinator therefore composes automatically, and no resolution function +// needs to know how deep it sits. + +/// Max identifier hops while chasing `var a = b, b = e.key` aliases. Bounds the +/// self-recursion in [`relative_arg_path`] so a cyclic reassignment can't loop. +const MAX_ALIAS_DEPTH: u32 = 8; + +/// Prepend `prefix` to every argument path in `nodes` (and their attrs/content), +/// in place. This is how a template's self-relative paths become absolute: the +/// caller knows which argument object it handed the template, the template does not. +fn prefix_arg_paths(nodes: &mut [WapChildNode], prefix: &[WapArgSegment]) { + if prefix.is_empty() { + return; + } + for n in nodes { + prefix_one(&mut n.arg_path, prefix); + for a in &mut n.attrs { + prefix_one(&mut a.arg_path, prefix); + } + if let Some(c) = n.content.as_mut() { + prefix_one(&mut c.arg_path, prefix); + } + for g in &mut n.variant_groups { + for v in &mut g.variants { + for a in &mut v.attrs { + prefix_one(&mut a.arg_path, prefix); + } + prefix_arg_paths(&mut v.children, prefix); + } + } + prefix_arg_paths(&mut n.children, prefix); + } +} + +/// The prefix a helper call supplies for the subtree it returns: the argument path of +/// its first argument. +/// +/// The empty path is a RESULT, not a failure. `helper(e)` hands over the caller's whole +/// argument object, so the callee's frame IS the caller's and the right prefix is +/// nothing — prefixing with it is a no-op and the paths are already absolute. `None` is +/// the different answer: the argument is absent or cannot be addressed at all +/// (`helper({jid: e.userJid})`), so the subtree's paths cannot be made absolute. What a +/// caller does with `None` differs by kind; see the two call sites. +fn helper_prefix( + call: &oxc_ast::ast::CallExpression, + scope: &VarScope, + module_source: &str, + ref_off: Option, +) -> Option { + let e = call.arguments.first().and_then(arg_expr)?; + relative_arg_path(e, scope, module_source, ref_off, 0) +} + +/// Enforce the argument-object boundary at a request root: a builder that does not take +/// exactly one options object publishes NO argument paths. +/// +/// Paths resolved directly in such a frame are already absent — there is no root to +/// resolve against. What this catches is the ones that arrive from elsewhere: a subtree +/// inlined from a cross-module helper or a `.map()` mapper carries paths relative to +/// ITS OWN parameter, and a two-parameter builder like +/// `function f(e, t){ … wap("sync", null, e.map(g)) }` cannot name what it passed, so +/// those paths would surface as though they addressed the request's arguments. The +/// legacy `WAWeb*Job` family is exactly this shape. +/// +/// Applied at the root rather than inside the resolver because only the root knows it is +/// a request: the same "cannot name it" state inside a mixin's `merge(dst, args)` is not +/// a failure at all — [`apply_contribution`] prefixes that contribution at the merge site. +pub(crate) fn enforce_argument_boundary( + children: &mut [WapChildNode], + scope: &VarScope, + builder_off: usize, +) { + if scope.arg_root_at(builder_off).is_none() { + clear_arg_paths(children); + } + drop_empty_arg_paths(children); +} + +/// Turn an empty path into no path. +/// +/// An empty path is a real intermediate result — "this is the argument object itself", +/// which is how a template says its parameter is the payload — and it composes: the +/// caller prefixes it into a real address. What it cannot be is a published one, because +/// there is nothing for a consumer to write at the root of its own arguments. Anything +/// still empty at a request root was never prefixed by anyone. +pub(crate) fn drop_empty_arg_paths(nodes: &mut [WapChildNode]) { + let empty = |p: &mut Option| { + if p.as_ref().is_some_and(|p| p.is_empty()) { + *p = None; + } + }; + for n in nodes { + empty(&mut n.arg_path); + for a in &mut n.attrs { + empty(&mut a.arg_path); + } + if let Some(c) = n.content.as_mut() { + empty(&mut c.arg_path); + } + for g in &mut n.variant_groups { + for v in &mut g.variants { + for a in &mut v.attrs { + empty(&mut a.arg_path); + } + drop_empty_arg_paths(&mut v.children); + } + } + drop_empty_arg_paths(&mut n.children); + } +} + +/// Drop every argument path in a subtree. Used where a subtree's paths are relative to a +/// frame this call site cannot name: a relative path published as an absolute one is a +/// wrong address, which is worse than none — an absent path is counted. +pub(crate) fn clear_arg_paths(nodes: &mut [WapChildNode]) { + for n in nodes { + n.arg_path = None; + for a in &mut n.attrs { + a.arg_path = None; + } + if let Some(c) = n.content.as_mut() { + c.arg_path = None; + } + for g in &mut n.variant_groups { + for v in &mut g.variants { + for a in &mut v.attrs { + a.arg_path = None; + } + clear_arg_paths(&mut v.children); + } + } + clear_arg_paths(&mut n.children); + } +} + +fn prefix_one(path: &mut Option, prefix: &[WapArgSegment]) { + if let Some(p) = path.as_mut() { + let mut full = prefix.to_vec(); + full.append(p); + *p = full; + } +} + +/// The argument path an expression reads, relative to the argument root of the +/// function `ref_off` sits in. `Some(vec![])` means "the argument object itself" +/// (a bare reference to the parameter). `None` means not structurally recoverable — +/// the caller counts it rather than inventing one. +fn relative_arg_path( + e: &Expression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + depth: u32, +) -> Option { + relative_arg_path_with( + e, + scope, + module_source, + ref_off, + depth, + RootRule::SoleParameter, + ) +} + +/// [`relative_arg_path`] under an explicit root rule, threaded unchanged through the +/// recursion so an alias chain resolves against the same frame the reference does. +fn relative_arg_path_with( + e: &Expression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + depth: u32, + rule: RootRule, +) -> Option { + if depth > MAX_ALIAS_DEPTH { + return None; + } + let (fn_start, _, root) = scope.root_at(ref_off?, rule)?; + + if let Expression::ParenthesizedExpression(p) = e { + return relative_arg_path_with(&p.expression, scope, module_source, ref_off, depth, rule); + } + + // `` itself → the whole argument object. + if let Some(name) = as_identifier(e) { + if name == root { + return Some(Vec::new()); + } + // A local alias: `var a = t.participantArgs`. Only initializers written INSIDE + // the enclosing function count. The module scope is flat and name-keyed, and the + // minifier reuses `a`/`i`/`t` in every sibling builder of the same module, so + // without the lexical bound one function's `i = t.descriptionArgs` and another's + // `i = optionalMerge(…)` are the same entry — and the first one that happens to + // resolve wins, pointing a child at a stranger's argument. + let inits = scope.vars.get(name)?; + let mut found: Option = None; + for init in inits { + if !scope.same_fn(fn_start, init.init_start) { + continue; + } + if let Some((s, en)) = init.owner_fn + && !matches!(ref_off, Some(o) if s <= o && o < en) + { + continue; + } + let slice = &module_source[init.init_start..init.init_end]; + let alloc = Allocator::default(); + let parsed = wa_oxc::parse_cjs(&alloc, slice); + let Some(expr) = first_expression(&parsed.program) else { + continue; + }; + let Some(p) = + relative_arg_path_with(expr, scope, module_source, ref_off, depth + 1, rule) + else { + continue; + }; + // A name assigned twice from DIFFERENT arguments (`var a = e.old; a = + // e.current;`) has no single answer here: `ref_off` is the enclosing + // function, not the use site, so which assignment reaches the read is not + // computable from what this holds. Taking the first would publish an address + // that is right about half the time, and a consumer following it writes into + // the wrong argument with nothing to warn it. Agreeing initializers (the same + // path recorded twice, which the flat scope produces routinely) are not + // ambiguous. + match &found { + Some(prev) if *prev != p => return None, + Some(_) => {} + None => found = Some(p), + } + } + return found; + } + + // `.a.b` → ["a", "b"]. Computed members (`x[i]`) are not a static key and + // stop the walk. + if e.as_member_expression().is_some() { + let mut keys = Vec::new(); + let mut cur = e; + while let Some(m) = cur.as_member_expression() { + let prop = m.static_property_name()?; + keys.push(prop.to_string()); + cur = m.object(); + } + keys.reverse(); + // The BASE is resolved by a restricted walk that refuses calls, and that + // restriction is the whole point. `var t = attrFromReference(attrStanzaId, e, + // ["id"]); … STANZA_ID(t.value)` reads a value out of the stanza being ACKED, not + // out of the builder's options object — but the general walk descends into a + // call's arguments, finds the bare parameter there and reports the empty path, + // which then absorbs `.value` and publishes `value` as an address. A call's + // result is a computed value; only the parameter itself, or a chain rooted at it, + // can be the object a key is read off. + let base = base_arg_path(cur, scope, module_source, ref_off, depth + 1, rule)?; + let mut out = base; + out.extend( + keys.into_iter() + .map(|key| WapArgSegment { key, list: false }), + ); + return Some(out); + } + + // A value coercion — `WAWap.JID(t)`, `OPTIONAL(WAWap.JID, n)`, `CUSTOM_STRING(t)`. + // A coercion-function reference (`o("WAWap").JID`) is rooted at a `require` call, not + // at the parameter, so it never resolves and cannot be mistaken for the value. + // + // Exactly one argument may resolve. A call that reads two — `combine(e.left, + // e.right)` — has no single source, and naming the first would describe a value the + // builder does not produce from it alone: a consumer supplying only that path gets a + // different stanza. Same rule as the conditional below, for the same reason. + if let Some(call) = as_call(e) { + let mut found: Option = None; + for a in &call.arguments { + let Some(ae) = arg_expr(a) else { continue }; + let Some(p) = + relative_arg_path_with(ae, scope, module_source, ref_off, depth + 1, rule) + else { + // An argument that MENTIONS the root but resolved to nothing — + // `combine(e.primary, e.values[i])`, where computed access has no static + // key — is a second source the resolver merely could not read. Publishing + // the one it did read describes a value the builder does not produce from + // that path alone. Same rule the conditional below applies to its arms. + if mentions(ae, root) { + return None; + } + continue; + }; + match &found { + Some(prev) if *prev != p => return None, + Some(_) => {} + None => found = Some(p), + } + } + return found; + } + + // `cond ? a : b`. Both arms reading the SAME argument is one address. Divergent arms + // are two, and one path cannot describe two sources. + // + // A one-sided result is only safe when the other arm is not a source at all — the + // `cond ? CUSTOM_STRING(e.value) : DROP_ATTR` shape, where the alternate is a module + // constant. An arm that MENTIONS the argument root but failed to resolve + // (`flag ? e.primary : e.values[i]`, where computed access has no static key) is a + // real alternative the resolver merely could not read, and publishing the arm that + // did read points a consumer at a source the builder may not use. + if let Expression::ConditionalExpression(c) = e { + let arm = |x| relative_arg_path_with(x, scope, module_source, ref_off, depth + 1, rule); + let (a, b) = (arm(&c.consequent), arm(&c.alternate)); + return match (a, b) { + (Some(x), Some(y)) if x == y => Some(x), + (Some(x), None) if !mentions(&c.alternate, root) => Some(x), + (None, Some(y)) if !mentions(&c.consequent, root) => Some(y), + _ => None, + }; + } + + None +} + +/// The argument path of the OBJECT a member chain is read off: the enclosing frame's +/// parameter, or an alias that itself resolves to one. +/// +/// Deliberately narrower than [`relative_arg_path`] — no calls, no conditionals. Those +/// produce a computed value, and a key read off a computed value is not an address into +/// the argument object however the value was derived. See the call site for the shape +/// that made this necessary. +fn base_arg_path( + e: &Expression, + scope: &VarScope, + module_source: &str, + ref_off: Option, + depth: u32, + rule: RootRule, +) -> Option { + if depth > MAX_ALIAS_DEPTH { + return None; + } + if let Expression::ParenthesizedExpression(p) = e { + return base_arg_path(&p.expression, scope, module_source, ref_off, depth, rule); + } + let (fn_start, _, root) = scope.root_at(ref_off?, rule)?; + if let Some(name) = as_identifier(e) { + if name == root { + return Some(Vec::new()); + } + let mut found: Option = None; + for init in scope.vars.get(name)? { + if !scope.same_fn(fn_start, init.init_start) { + continue; + } + if let Some((s, en)) = init.owner_fn + && !matches!(ref_off, Some(o) if s <= o && o < en) + { + continue; + } + let slice = &module_source[init.init_start..init.init_end]; + let alloc = Allocator::default(); + let parsed = wa_oxc::parse_cjs(&alloc, slice); + let Some(expr) = first_expression(&parsed.program) else { + continue; + }; + let Some(p) = base_arg_path(expr, scope, module_source, ref_off, depth + 1, rule) + else { + continue; + }; + // Same disagreement rule as the general walk: two different sources for one + // name is no address at all. + match &found { + Some(prev) if *prev != p => return None, + Some(_) => {} + None => found = Some(p), + } + } + return found; + } + if e.as_member_expression().is_some() { + return relative_arg_path_with(e, scope, module_source, ref_off, depth, rule); + } + None +} + +/// Rebase a template's or mapper's subtree onto the caller's frame, and return the path +/// the caller handed it. +/// +/// `arg` is the expression the combinator passed (a `REPEATED_CHILD` list, an +/// `OPTIONAL_CHILD` argument object, a `.map()` receiver); `list` marks that path's last +/// segment as the one a consumer indexes. +/// +/// The three outcomes match the cross-module helper's, for the same reason — the callee +/// resolved its paths against its own parameter and only the call site knows what that +/// was: +/// +/// - the argument names a path → prefix, and that path is the node's own; +/// - the argument IS the caller's whole object (`merge…Mixin(dst, e)`, `OPTIONAL_CHILD(t, +/// e)`) → the empty path, which is a no-op prefix rather than a failure: the callee's +/// frame is the caller's, so its paths are already absolute and are left as they are. +/// The node itself gets no path — its object is the request's arguments, which is not +/// an address a consumer supplies; +/// - it does not name one, and the caller HAS an argument object → the subtree's paths +/// are relative to something this frame cannot name, so they are cleared. Left alone +/// they read as top-level addresses into the request's arguments, which is what +/// published `collection`/`version` on a mapped `` as though a consumer +/// should write them at the root; +/// - it does not, and the caller has no argument object → a mixin's `merge(dst, args)` +/// frame, where nothing can be said. [`apply_contribution`] prefixes the whole +/// contribution at the merge site that knows. +/// +/// A LIST is the exception to the empty case: `REPEATED_CHILD(t, e)` iterates the +/// argument object itself, so the template's keys address an element rather than the +/// request — and with no segment to carry the `[]` there is nothing that can say so. +/// That is the "cannot name it" outcome, and the paths are cleared. +fn rebase_template( + nodes: &mut [WapChildNode], + arg: Option<&Expression>, + list: bool, + scope: &VarScope, + module_source: &str, + ref_off: Option, +) -> Option { + let resolved = arg + .and_then(|e| relative_arg_path(e, scope, module_source, ref_off, 0)) + .map(|mut p| { + if list && let Some(last) = p.last_mut() { + last.list = true; + } + p + }); + match resolved.as_deref() { + Some([]) if !list => return None, + Some([]) | None if ref_off.and_then(|o| scope.arg_root_at(o)).is_some() => { + clear_arg_paths(nodes); + return None; + } + Some([]) | None => return None, + Some(prefix) => prefix_arg_paths(nodes, prefix), + } + resolved +} + +/// Whether an expression references `root` anywhere — the identifier that names the +/// enclosing frame's argument object. +/// +/// Used to tell "this arm reads no argument" from "this arm reads one the resolver could +/// not spell out". Compares identifier names over the expression's own AST rather than +/// resolving them, because the callers hold a re-parsed slice whose spans do not index +/// the module — so a name that merely happens to match the root's spelling counts, which +/// is the conservative side of the question being asked. +fn mentions(e: &Expression, root: &str) -> bool { + struct Finder<'a> { + root: &'a str, + found: bool, + } + impl<'a> Visit<'a> for Finder<'_> { + fn visit_identifier_reference(&mut self, id: &oxc_ast::ast::IdentifierReference<'a>) { + self.found = self.found || id.name == self.root; + } + } + let mut f = Finder { root, found: false }; + f.visit_expression(e); + f.found +} + +/// Fill in `arg_path` on each attribute of a node whose attrs came from `attrs_node`. +/// +/// A `Const` attribute is skipped: the builder writes the literal itself, so there is +/// no argument to point at, and an absent path there means "nothing to supply" rather +/// than "we failed". +pub(crate) fn is_optional_literal(value: &Expression, aliases: &AliasMap) -> bool { + as_call(value).is_some_and(|call| { + callee_method(call) == Some("OPTIONAL_LITERAL") + && callee_object(call).and_then(|o| resolve_owner(o, aliases)) == Some("WASmaxAttrs") + }) +} + +pub(crate) fn annotate_attr_arg_paths( + attrs: &mut [WapAttrDef], + attrs_node: &Expression, + aliases: &AliasMap, + scope: &VarScope, + module_source: &str, + ref_off: Option, +) { + let Expression::ObjectExpression(obj) = attrs_node else { + return; + }; + for prop in &obj.properties { + let ObjectPropertyKind::ObjectProperty(p) = prop else { + continue; + }; + let PropertyKey::StaticIdentifier(key) = &p.key else { + continue; + }; + let Some(attr) = attrs.iter_mut().find(|a| a.name == key.name.as_str()) else { + continue; + }; + if attr.kind == WapAttrKind::Const { + continue; + } + // `WASmaxAttrs.OPTIONAL_LITERAL("true", e.hasDelete)` writes a FIXED wire value + // and takes a boolean deciding whether to write it at all. The generic call walk + // finds the boolean and would report it as the attribute's value address — so a + // consumer reads "put this attribute's value at `hasDescriptionDeleteTrue`" and + // supplies the wire string, when what the builder wants there is a flag. That is + // the attribute analogue of a presence marker, and `arg_path` has no way to say + // so; naming nothing is the honest answer until it does. + if is_optional_literal(&p.value, aliases) { + continue; + } + // An empty path (a bare reference to the argument object) is not an + // attribute source; only a keyed read is. + if let Some(path) = relative_arg_path(&p.value, scope, module_source, ref_off, 0) + && !path.is_empty() + { + attr.arg_path = Some(path); + } + } +} + /// Resolve a `wap()` child-argument expression into zero or more child nodes. /// /// - `node_source`: the source `node` was parsed from (for span-relative ops). @@ -313,6 +1105,13 @@ pub(crate) fn resolve_child_node( ref_off, ) }; + // Presence is deliberately NOT weakened here. A `cond ? wap("x", …) : null` child + // can indeed be omitted, but by a control-flow guard rather than by one of the + // `WASmaxChildren` combinators `presence` is defined over — there is no argument + // object behind it, so calling it `Optional` would answer "which combinator built + // this" with something that is not one. The seam shows in the counters: 17 such + // children would be reported as combinator children missing an argument address + // they can never have. Extracting the guard is a different question from shape. let mut out = resolve(&cond.consequent); for child in resolve(&cond.alternate) { if !out.contains(&child) { @@ -326,10 +1125,13 @@ pub(crate) fn resolve_child_node( if let Some(call) = as_call(node) && let Some(wap) = parse_wap_call(call, aliases) { - let attrs = wap + let mut attrs = wap .attrs_node .map(|n| extract_attrs_from_obj(n, node_source, aliases)) .unwrap_or_default(); + if let Some(n) = wap.attrs_node { + annotate_attr_arg_paths(&mut attrs, n, aliases, scope, module_source, ref_off); + } let mut children = Vec::new(); for child_arg in wap.child_args { if let Some(ce) = arg_expr(child_arg) { @@ -347,7 +1149,7 @@ pub(crate) fn resolve_child_node( } } let content = if children.is_empty() { - leaf_content(wap.child_args) + leaf_content(wap.child_args, scope, module_source, ref_off, aliases) } else { None }; @@ -358,6 +1160,7 @@ pub(crate) fn resolve_child_node( content, repeats: false, variant_groups: Vec::new(), + ..Default::default() }]; } @@ -410,6 +1213,7 @@ pub(crate) fn resolve_child_node( contributions, helpers, depth, + ref_off, ) { return m; } @@ -443,6 +1247,7 @@ pub(crate) fn resolve_child_node( contributions, helpers, depth, + ref_off, ) { return m; } @@ -463,9 +1268,9 @@ pub(crate) fn resolve_child_node( { let owner = callee_object(call).and_then(|o| resolve_owner(o, aliases)); let repeated = owner == Some("WASmaxChildren") && method == "REPEATED_CHILD"; - let optional = owner == Some("WASmaxChildren") - && matches!(method, "OPTIONAL_CHILD" | "HAS_OPTIONAL_CHILD"); - if (repeated || optional) + let optional = owner == Some("WASmaxChildren") && method == "OPTIONAL_CHILD"; + let presence_flag = owner == Some("WASmaxChildren") && method == "HAS_OPTIONAL_CHILD"; + if (repeated || optional || presence_flag) && let Some(first) = call.arguments.first().and_then(arg_expr) { let mut r = resolve_template_arg( @@ -479,9 +1284,41 @@ pub(crate) fn resolve_child_node( depth, ref_off, ); + // The 2nd argument is what the combinator feeds the template: the list for + // REPEATED_CHILD, the argument object for OPTIONAL_CHILD, the boolean for + // HAS_OPTIONAL_CHILD. It is therefore both this node's own argument path + // and the prefix that turns the template's self-relative paths absolute — + // the template resolved above knows nothing about where it was called from. + // + // `[]` marks the ONE segment a consumer must index: REPEATED_CHILD calls the + // template once per element, so everything the template reads lives on an + // element. OPTIONAL_CHILD hands the object over whole and gets no marker — + // the same suffix in the wrong place writes the value where the vendor + // builder never reads it. + let node_path = rebase_template( + &mut r, + call.arguments.get(1).and_then(arg_expr), + repeated, + scope, + module_source, + ref_off, + ); if repeated { + let (min, max) = repeat_bounds(call); for c in &mut r { c.repeats = true; + c.repeat_min = min; + c.repeat_max = max; + c.arg_path = node_path.clone(); + } + } else { + for c in &mut r { + c.presence = if optional { + WapChildPresence::Optional + } else { + WapChildPresence::PresenceFlag + }; + c.arg_path = node_path.clone(); } } if !r.is_empty() { @@ -554,7 +1391,28 @@ pub(crate) fn resolve_child_node( if let Some(name) = mixin && let Some(contrib) = contribs.get(&name) { - apply_contribution(&mut out, contrib, is_optional_merge); + // A mixin resolves its own paths against the argument object the + // caller hands it — the 2nd argument of `merge…(dst, args)`, the + // 3rd of `optionalMerge(mergeFn, dst, args)`. Prefixing here is what + // makes `namedSubjectOrUnnamedSubjectFallbackMixinGroupArgs` + // reachable from the request root; without it the mixin's paths + // would silently claim to start at the request's own arguments. + // Same three outcomes as every other frame-handing call site, via the + // same helper: prefix when the argument names a path, clear when this + // frame has an argument object but cannot name what it passed, and + // leave alone when it has none — the two-parameter merge frame, which + // a caller further out rebases. + let args_idx = if is_optional_merge { 2 } else { 1 }; + let mut contrib = contrib.clone(); + rebase_template( + &mut contrib, + call.arguments.get(args_idx).and_then(arg_expr), + false, + scope, + module_source, + ref_off, + ); + apply_contribution(&mut out, &contrib, is_optional_merge); } } if !out.is_empty() { @@ -564,13 +1422,26 @@ pub(crate) fn resolve_child_node( } // Case 5: `helper(args)` — trace the helper's return value. + // + // The helper resolves its paths against ITS OWN parameter, so they arrive relative + // to whatever this call passed. That argument is the prefix, exactly as it is for a + // `REPEATED_CHILD` list or a mixin's argument object: without it `helper(e.userArgs)` + // over a helper reading `arg.jid` publishes a bare `jid`, which reads as an absolute + // address into the request's own arguments and is not one. + // + // Unaddressable argument: leave the paths, do NOT clear. A local helper is resolved + // in the same scope as its caller, and the case that matters is a mixin's + // `merge(dst, args)` calling `build(args)` — a two-parameter frame has no argument + // root, so nothing can be named here, and `apply_contribution` prefixes the whole + // contribution at the merge site that does know. Clearing instead destroys 63 correct + // paths. The cross-module branch below is the opposite case and does clear. if let Some(call) = as_call(node) && let Some(callee_name) = as_identifier(&call.callee) && let Some(inits) = scope.vars.get(callee_name) { for vi in inits { if let Some((bs, be)) = vi.fn_body { - let r = resolve_function_return( + let mut r = resolve_function_return( bs, be, scope, @@ -581,6 +1452,9 @@ pub(crate) fn resolve_child_node( depth + 1, ); if !r.is_empty() { + if let Some(prefix) = helper_prefix(call, scope, module_source, ref_off) { + prefix_arg_paths(&mut r, &prefix); + } return r; } } @@ -590,16 +1464,84 @@ pub(crate) fn resolve_child_node( // Case 5b: cross-module helper `o("Module").fn(args)` — inline the wap subtree // the helper returns (e.g. `o("WAWebSignalUtilsApi").xmppSignedPreKey(t)` → the // `` tree), read from the pre-built index. + // + // The index is built once per (module, fn) with no call site in view, so any path in + // it is relative to that helper's own parameter and needs the same prefixing. When + // this call site's argument is not itself addressable, the paths are dropped rather + // than published as though they were absolute — and here that is right where it is + // wrong above, because nothing downstream will ever rebase a subtree that came from + // the index: `xmppSignedPreKey(t)` in a job generator reads a key pair the job + // fetched, not an argument anybody passes. if let Some(call) = as_call(node) && let Some((module, func)) = require_member_call(&call.callee) && let Some(tree) = helpers.get(&module, &func) { - return tree.clone(); + let mut tree = tree.clone(); + match helper_prefix(call, scope, module_source, ref_off) { + Some(prefix) => prefix_arg_paths(&mut tree, &prefix), + None => clear_arg_paths(&mut tree), + } + return tree; } Vec::new() } +/// The `min`/`max` of `REPEATED_CHILD(template, list, min, max)`. +/// +/// Three outcomes, because two of them must not look alike: +/// +/// - both literals → a closed range; +/// - a literal `min` with `1/0` as the `max` → "at least min, no ceiling". WA writes an +/// unbounded maximum as the division, so this is a bound the builder STATES; +/// - anything else → neither bound is emitted, and the site is counted. +/// +/// The third case is why the maximum is not simply "literal or nothing". A computed but +/// finite maximum would otherwise serialize exactly like the stated infinity above, and +/// a consumer would read a real ceiling as its absence — the precise conflation between +/// "no constraint" and "a constraint we could not extract" this IR exists to avoid. No +/// call site in the current bundle takes that form (all 65 are literals or `1/0`), so +/// suppressing it costs nothing today and keeps the claim true if one appears. +fn repeat_bounds(call: &oxc_ast::ast::CallExpression) -> (Option, Option) { + let lit = |i: usize| { + call.arguments + .get(i) + .and_then(arg_expr) + .and_then(as_int) + .and_then(|n| u32::try_from(n).ok()) + }; + let max_arg = call.arguments.get(3).and_then(arg_expr); + match (lit(2), lit(3)) { + (Some(min), Some(max)) => (Some(min), Some(max)), + // `1/0` (or a bare `Infinity`) is a stated bound, not a failure to read one — but + // the minimum still has to be readable, or there is no range to state. + (Some(min), None) if max_arg.is_some_and(is_infinity) => (Some(min), None), + // Bounds move together. A ceiling with no floor is half a range, and it would be + // counted as a recovered bound while the floor the builder also enforces is + // silently absent. + _ => (None, None), + } +} + +/// Whether an expression is JavaScript's positive infinity as a minifier writes it: +/// `1/0`, or the global `Infinity`. Structural — a variable that merely holds infinity +/// at runtime is not recognized, and is treated as an unresolved bound. +/// +/// The numerator has to be *positive*: `-1/0` is negative infinity, an upper bound no +/// list length can satisfy, and reading it as "unbounded" would state the opposite of +/// what the builder enforces. +fn is_infinity(e: &Expression) -> bool { + if let Expression::Identifier(id) = e { + return id.name == "Infinity"; + } + let Expression::BinaryExpression(b) = e else { + return false; + }; + b.operator == oxc_syntax::operator::BinaryOperator::Division + && as_int(&b.left).is_some_and(|n| n > 0) + && as_int(&b.right) == Some(0) +} + /// `o("Module").fn` (a member whose object is a `require` call) → `(Module, fn)`. /// The child-position form of a cross-module helper reference. fn require_member_call(callee: &Expression) -> Option<(String, String)> { @@ -637,6 +1579,7 @@ fn apply_contribution(dst: &mut Vec, contrib: &[WapChildNode], opt // by the time a request resolves. let mut r = root.clone(); mark_groups_optional(&mut r.variant_groups, optional); + mark_optional(&mut r, optional); dst.push(r); } else { for d in dst.iter_mut() { @@ -647,8 +1590,12 @@ fn apply_contribution(dst: &mut Vec, contrib: &[WapChildNode], opt merge_node_into(d, root, optional); } else { // No matching destination tag: append the node (mergeStanzas semantics). + // `optionalMerge` can skip the whole contribution, so a tag it INTRODUCES is + // one the request may not carry — unlike one that merges onto a destination + // built at its own call site, which keeps that site's cardinality. let mut r = root.clone(); mark_groups_optional(&mut r.variant_groups, optional); + mark_optional(&mut r, optional); dst.push(r); } } @@ -666,6 +1613,7 @@ fn merge_node_into(d: &mut WapChildNode, src: &WapChildNode, optional: bool) { // promotes a locally-singular child, mirroring `mixin_index::merge_children` so a // repeated child folded onto an existing tag isn't silently treated as singular. d.repeats = d.repeats || src.repeats; + adopt_builder_facts(d, src); crate::mixin_index::merge_children(&mut d.children, &src.children); for g in &src.variant_groups { let mut g2 = g.clone(); @@ -674,6 +1622,52 @@ fn merge_node_into(d: &mut WapChildNode, src: &WapChildNode, optional: bool) { } } +/// Take from a merged-in fragment the builder facts the destination doesn't have: +/// element content and repeat bounds. +/// +/// Fill-if-empty rather than overwrite — the destination is the node built at the call +/// site and is the more specific description; a fragment only ever ADDS, mirroring +/// `mergeStanzas`. This is where a mixin's element value stops disappearing: the merge +/// used to union attrs and children and drop everything else, so a `` built in +/// a mixin arrived at the request as a bare tag. +/// +/// **`presence` is deliberately not among them.** It has no "unset" — `Required` is +/// both a real state and the serde default — so a fill-if-empty test cannot tell a +/// destination that is genuinely unconditional from one that was never classified, and +/// would happily weaken the first. It does not need to: this runs only when a +/// destination node with that tag already exists, which means it was built at its own +/// call site, and a child some call site emits unconditionally is unconditional +/// regardless of what another site does with it. A fragment whose tag has no destination +/// is appended whole by [`apply_contribution`], keeping its own presence intact. +pub(crate) fn adopt_builder_facts(d: &mut WapChildNode, src: &WapChildNode) { + if d.content.is_none() { + d.content = src.content.clone(); + } + // Bounds move together or not at all. A destination with a minimum and no maximum is + // the contract's *explicit* unbounded range (WA's `1/0`), not a half-filled one, so + // taking a fragment's finite ceiling would quietly cap a list its own call site left + // open. Only a destination that states no bound at all can adopt one. + if d.repeat_min.is_none() && d.repeat_max.is_none() { + d.repeat_min = src.repeat_min; + d.repeat_max = src.repeat_max; + } + // `arg_path` is deliberately NOT adopted. Content and bounds are wire facts about the + // element — the merged stanza really does carry that value, really is bounded that + // way — but a path is an address for the node's own argument object, and a node two + // sites build together has no single one. The destination was built at its own call + // site; taking the fragment's address would tell a consumer to hand that object over + // whole and get the destination's locally-built half for free, which it would not. +} + +/// Weaken a node the caller knows may be skipped. Only ever `Required` → `Optional`: a +/// presence marker or an already-optional child was classified at its own call site and +/// says something this does not. +fn mark_optional(node: &mut WapChildNode, optional: bool) { + if optional && node.presence.is_required() { + node.presence = WapChildPresence::Optional; + } +} + fn mark_groups_optional(groups: &mut [wa_ir::WapVariantGroup], optional: bool) { if optional { for g in groups { @@ -751,6 +1745,7 @@ fn resolve_map_call( contributions: Option<&MixinContributions>, helpers: &HelperIndex, depth: u32, + ref_off: Option, ) -> Option> { let call = as_call(node)?; if callee_method(call)? != "map" { @@ -762,7 +1757,17 @@ fn resolve_map_call( Expression::FunctionExpression(func) => { let body = func.body.as_ref()?; let body_code = &node_source[body.span.start as usize..body.span.end as usize]; - find_wap_calls_in_body(body_code, aliases) + // The callback's own parameter is the element it maps, and every key it reads + // hangs off that; `rebase_template` then prefixes the receiver. + let element = param_names(&func.params); + find_wap_calls_in_body( + body_code, + aliases, + match element.as_slice() { + [only] => Some(only.as_str()), + _ => None, + }, + ) } // Reference mapper — a cross-module helper or a local function. _ => resolve_mapper_ref( @@ -778,8 +1783,22 @@ fn resolve_map_call( if children.is_empty() { return None; } + // The mapper reads one ELEMENT of the receiver, so its paths are relative to that + // element and the receiver is their prefix — the same rebasing a `REPEATED_CHILD` + // template needs, and for the same reason. Without it a mapped `` + // published `collection`/`version` as though a consumer wrote them at the root of the + // request's arguments. + let list_path = rebase_template( + &mut children, + callee_object(call), + true, + scope, + module_source, + ref_off, + ); for c in &mut children { c.repeats = true; + c.arg_path = list_path.clone(); } Some(children) } @@ -875,6 +1894,16 @@ fn resolve_function_returns_each( .or_default() .extend(ginits.iter().cloned()); } + // Argument roots follow the same shift-then-inherit rule as the vars. The body slice + // re-parse cannot see this function's OWN parameter (it lives in the header, outside + // the span), so the module scope's entry is the one that answers `arg_root_at` here — + // dropping it made every path resolved through a template return come out empty. + for (s, e, name) in &mut merged.fn_params { + *s += body_start; + *e += body_start; + let _ = name; + } + merged.fn_params.extend(scope.fn_params.iter().cloned()); let mut per_return = Vec::new(); for arg_src in collect_return_arg_sources(body) { let alloc2 = Allocator::default(); @@ -938,7 +1967,7 @@ fn resolve_function_return( } // No resolvable returns: fall back to a flat scan, then `return helper(...)` chains. let body = &module_source[body_start..body_end]; - let direct = find_wap_calls_in_body(body, aliases); + let direct = find_wap_calls_in_body(body, aliases, None); if !direct.is_empty() { return direct; } @@ -1147,16 +2176,40 @@ impl<'a> Visit<'a> for MergeFnFinder<'_> { /// The body is re-parsed in isolation, so smax aliases local to the enclosing /// module aren't visible; we rebuild a local alias map from this body so any /// `(X = o("WASmaxJsx"))` inside it is still resolved. -fn find_wap_calls_in_body(body_code: &str, outer: &AliasMap) -> Vec { +fn find_wap_calls_in_body( + body_code: &str, + outer: &AliasMap, + arg_root: Option<&str>, +) -> Vec { let alloc = Allocator::default(); let ret = wa_oxc::parse_cjs(&alloc, body_code); - let local = build_alias_map(&ret.program); + // Layered over the enclosing module's aliases: the callback calls `WASmaxAttrs` + // through a name declared outside the body it is re-parsed from, so a body-only map + // reads `A.OPTIONAL_LITERAL(…)` as an ordinary dynamic value and publishes its + // presence flag as the attribute's address. + // The body is re-parsed WITHOUT its header, so the callback's own parameter is not + // among the names it binds — and that parameter is the likeliest collision of all, + // since the minifier spells it with the same letters it spells the module's aliases. + let mut shadowed = crate::alias::bound_names(&ret.program); + shadowed.extend(arg_root.map(str::to_string)); + let local = build_alias_map(&ret.program).over(outer, &shadowed); + // A scope over this body ALONE, with the callback's parameter as its argument root. + // That is all a mapper needs: it reads one element and nothing else, so every path it + // states is relative to that parameter — and the caller rebases the lot onto the + // receiver afterwards. Without the root the collector produced nodes whose keys were + // simply absent, so a consumer could find the list and not what to put in it. + let mut scope = build_var_scope(&ret.program); + if let Some(root) = arg_root { + scope + .fn_params + .push((0, body_code.len(), vec![root.to_string()])); + } let mut c = WapCollector { out: Vec::new(), source: body_code, - // Prefer the body-local aliases; fall back to the outer ones. - local: &local, - outer, + aliases: &local, + scope: &scope, + rooted: arg_root.is_some(), }; c.visit_program(&ret.program); c.out @@ -1165,25 +2218,52 @@ fn find_wap_calls_in_body(body_code: &str, outer: &AliasMap) -> Vec { out: Vec, source: &'s str, - local: &'s AliasMap, - outer: &'s AliasMap, + /// The body's own aliases layered over the enclosing module's, so every pass below + /// reads the same names — the tag, the attribute kinds and the paths. + aliases: &'s AliasMap, + /// The body's own scope, carrying the callback parameter as an argument root when + /// there is one. + scope: &'s VarScope, + /// Whether that root exists. Without it the sweep has no reference context and every + /// path is unrecoverable by construction, which is a different thing from failing to + /// resolve one. + rooted: bool, } impl<'a> Visit<'a> for WapCollector<'_> { fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) { - let parsed = parse_wap_call(call, self.local).or_else(|| parse_wap_call(call, self.outer)); + let parsed = parse_wap_call(call, self.aliases); if let Some(wap) = parsed { - let attrs = wap + let mut attrs = wap .attrs_node - .map(|n| extract_attrs_from_obj(n, self.source, self.local)) + .map(|n| extract_attrs_from_obj(n, self.source, self.aliases)) .unwrap_or_default(); + // Offset 0 is inside the synthetic body span, so it names the callback frame. + let ref_off = self.rooted.then_some(0); + if let Some(n) = wap.attrs_node { + annotate_attr_arg_paths( + &mut attrs, + n, + self.aliases, + self.scope, + self.source, + ref_off, + ); + } self.out.push(WapChildNode { tag: wap.tag.to_string(), attrs, children: Vec::new(), - content: leaf_content(wap.child_args), + content: leaf_content( + wap.child_args, + self.scope, + self.source, + ref_off, + self.aliases, + ), repeats: false, variant_groups: Vec::new(), + ..Default::default() }); } walk::walk_call_expression(self, call); @@ -1315,19 +2395,910 @@ mod tests { ) } - /// Like [`resolve`], but with cross-module mixin contributions available (Phase - /// 3): a `merge…Mixin(dst,…)` folds the mixin's attrs/children into `dst`. - fn resolve_with( - code: &str, - expr_src: &str, - contributions: &MixinContributions, - helpers: &HelperIndex, - ) -> Vec { + /// Resolve the return value of a named builder function in `code`, the way the IQ + /// scanner reaches a request's children — inside a function, so the argument-path + /// machinery has a reference context and an argument root to work from. [`resolve`] + /// deliberately has neither. + fn resolve_builder(code: &str, fn_name: &str) -> Vec { + resolve_builder_with(code, fn_name, &HelperIndex::default()) + } + + /// [`resolve_builder`] with a seeded cross-module helper index, so the branch that + /// inlines an indexed subtree can be exercised. + fn resolve_builder_with(code: &str, fn_name: &str, helpers: &HelperIndex) -> Vec { let alloc = Allocator::default(); let ret = wa_oxc::parse_cjs(&alloc, code); let scope = build_var_scope(&ret.program); let aliases = build_alias_map(&ret.program); - + let (bs, be) = scope + .vars + .get(fn_name) + .and_then(|inits| inits.iter().find_map(|vi| vi.fn_body)) + .unwrap_or_else(|| panic!("no function body for `{fn_name}`")); + let out = resolve_function_return(bs, be, &scope, code, &aliases, None, helpers, 0); + // A builder returns the ``; the tests are about its children, which is also + // what the IQ scanner keeps. + match out.as_slice() { + [root] if root.tag == "iq" => root.children.clone(), + _ => out, + } + } + + fn path_of(p: &Option) -> Vec<(String, bool)> { + p.iter() + .flatten() + .map(|s| (s.key.clone(), s.list)) + .collect() + } + + #[test] + fn element_value_survives_a_destructured_local() { + // `smax("subject", null, t)` where `t` was destructured off the builder's + // argument object. The payload of a group rename is the element's content and + // nothing else, so a builder that binds it to a local first must still yield it. + let code = r#" + function b(e){ var t = e.subjectElementValue; + return o("WASmaxJsx").smax("iq", null, o("WASmaxJsx").smax("subject", null, t)); } + "#; + let out = resolve_builder(code, "b"); + let subject = out + .iter() + .find(|c| c.tag == "subject") + .expect("subject child"); + let content = subject + .content + .as_ref() + .expect("`` carries its element value"); + assert_eq!( + path_of(&content.arg_path), + vec![("subjectElementValue".to_string(), false)] + ); + } + + #[test] + fn a_node_variable_is_still_a_child_not_content() { + // The other half of the identifier rule: a local bound to a `smax(…)` call is a + // CHILD. It roots at no parameter, so it yields no path and no content — the + // discrimination that lets the test above be safe. + let code = r#" + function b(e){ var n = o("WASmaxJsx").smax("body", null); + return o("WASmaxJsx").smax("description", null, n); } + "#; + let out = resolve_builder(code, "b"); + let desc = out.iter().find(|c| c.tag == "description").expect("desc"); + assert!(desc.content.is_none(), "content: {:?}", desc.content); + assert_eq!( + desc.children + .iter() + .map(|c| c.tag.as_str()) + .collect::>(), + vec!["body"] + ); + } + + #[test] + fn repeated_child_arg_path_marks_the_list_segment() { + // `REPEATED_CHILD(tmpl, list, min, max)` calls the template once per element, so + // the LIST segment is indexed and the template's own keys are not. + let code = r#" + function t(e){ var j = e.participantJid; + return o("WASmaxJsx").smax("participant", {jid: o("WAWap").JID(j)}); } + function b(e){ var a = e.participantArgs; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, a, 1, 1024)); } + "#; + let out = resolve_builder(code, "b"); + let p = out.iter().find(|c| c.tag == "participant").expect("child"); + assert!(p.repeats); + assert_eq!(p.presence, WapChildPresence::Required); + assert_eq!((p.repeat_min, p.repeat_max), (Some(1), Some(1024))); + assert_eq!( + path_of(&p.arg_path), + vec![("participantArgs".to_string(), true)], + "the node addresses the list itself" + ); + assert_eq!( + path_of(&p.attrs[0].arg_path), + vec![ + ("participantArgs".to_string(), true), + ("participantJid".to_string(), false) + ], + "`[]` on the list segment, never on the key read off an element" + ); + } + + #[test] + fn optional_child_arg_path_carries_no_list_marker() { + // The mirror of the test above, and the reason both exist: `OPTIONAL_CHILD` + // hands the argument object to the template WHOLE. Indexing it would write the + // value where the vendor builder never reads, so no segment may be marked. + let code = r#" + function t(e){ var i = e.descriptionId; + return o("WASmaxJsx").smax("description", {id: o("WAWap").CUSTOM_STRING(i)}); } + function b(e){ var d = e.descriptionArgs; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").OPTIONAL_CHILD(t, d)); } + "#; + let out = resolve_builder(code, "b"); + let d = out.iter().find(|c| c.tag == "description").expect("child"); + assert_eq!(d.presence, WapChildPresence::Optional); + assert!(!d.repeats); + assert_eq!((d.repeat_min, d.repeat_max), (None, None)); + assert_eq!( + path_of(&d.arg_path), + vec![("descriptionArgs".to_string(), false)] + ); + assert_eq!( + path_of(&d.attrs[0].arg_path), + vec![ + ("descriptionArgs".to_string(), false), + ("descriptionId".to_string(), false) + ] + ); + assert!( + d.arg_path + .iter() + .flatten() + .chain(d.attrs.iter().flat_map(|a| a.arg_path.iter().flatten())) + .all(|s| !s.list), + "no segment of an OPTIONAL_CHILD path may be marked as a list" + ); + } + + #[test] + fn the_three_cardinalities_are_distinguishable() { + // Required, optional and presence-marker children that are all empty on the + // wire. Without `presence` the three are the same `{tag, [], []}` node, and a + // consumer cannot tell which one it may model as a `bool`. + let code = r#" + function req(){ return o("WASmaxJsx").smax("always", null); } + function opt(e){ var x = e.optArgs; return o("WASmaxJsx").smax("maybe", null); } + function flag(){ return o("WASmaxJsx").smax("locked", null); } + function b(e){ var a = e.optArgs, l = e.hasLocked; + return o("WASmaxJsx").smax("iq", null, [req(), + o("WASmaxChildren").OPTIONAL_CHILD(opt, a), + o("WASmaxChildren").HAS_OPTIONAL_CHILD(flag, l)]); } + "#; + let out = resolve_builder(code, "b"); + let by = |tag: &str| { + out.iter() + .find(|c| c.tag == tag) + .unwrap_or_else(|| panic!("{tag}: {out:?}")) + .presence + }; + assert_eq!(by("always"), WapChildPresence::Required); + assert_eq!(by("maybe"), WapChildPresence::Optional); + assert_eq!(by("locked"), WapChildPresence::PresenceFlag); + assert_eq!( + path_of(&out.iter().find(|c| c.tag == "locked").unwrap().arg_path), + vec![("hasLocked".to_string(), false)], + "a presence marker addresses its boolean, not an argument object" + ); + } + + #[test] + fn an_unbounded_max_leaves_only_the_minimum() { + // WA writes an open upper bound as `1/0`. Recovering `min` anyway is what keeps + // "at least n, no ceiling" distinguishable from a `.map()` child that states + // no bound at all — the latter has neither. + let code = r#" + function t(e){ return o("WASmaxJsx").smax("item", null); } + function b(e){ var a = e.itemArgs; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, a, 0, 1/0)); } + "#; + let out = resolve_builder(code, "b"); + let item = out.iter().find(|c| c.tag == "item").expect("item"); + assert_eq!((item.repeat_min, item.repeat_max), (Some(0), None)); + } + + #[test] + fn a_computed_max_suppresses_both_bounds() { + // The counterpart of the test above, and the reason it can mean what it says: a + // maximum WA computes rather than writes is NOT infinity, and must not serialize + // like it. Dropping the minimum too is what stops a real ceiling from reading as + // its absence. + let code = r#" + function t(e){ return o("WASmaxJsx").smax("item", null); } + function b(e){ var a = e.itemArgs, n = e.cap; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, a, 0, n)); } + "#; + let out = resolve_builder(code, "b"); + let item = out.iter().find(|c| c.tag == "item").expect("item"); + assert_eq!((item.repeat_min, item.repeat_max), (None, None)); + } + + #[test] + fn a_negative_infinity_max_is_not_unbounded() { + // `-1/0` is negative infinity: an upper bound no list length can satisfy. It is + // written the same way as the open bound above and differs only in sign, so a + // check for "numerator over zero" reads it as the opposite of what it says. + let code = r#" + function t(e){ return o("WASmaxJsx").smax("item", null); } + function b(e){ var a = e.itemArgs; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, a, 0, -1/0)); } + "#; + let out = resolve_builder(code, "b"); + let item = out.iter().find(|c| c.tag == "item").expect("item"); + assert_eq!( + (item.repeat_min, item.repeat_max), + (None, None), + "an unreadable bound, not an open one" + ); + } + + /// Resolve a builder's return the way `module.rs` does when it walks an `` call: + /// against the MODULE scope, at the reference's real module offset. Distinct from + /// [`resolve_builder`], which re-parses the body first and so puts the body's own + /// declarations ahead of the module's — masking exactly the name collisions the flat + /// scope creates. + fn resolve_in_module(code: &str, fn_name: &str) -> Vec { + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, code); + let scope = build_var_scope(&ret.program); + let aliases = build_alias_map(&ret.program); + let fn_at = code + .find(&format!("function {fn_name}(")) + .unwrap_or_else(|| panic!("no `{fn_name}` in the source")); + let ret_at = code[fn_at..].find("return ").expect("a return") + fn_at + "return ".len(); + let end = code[ret_at..].find(";").expect("a terminator") + ret_at; + let owned = format!("({});", &code[ret_at..end]); + let alloc2 = Allocator::default(); + let ret2 = wa_oxc::parse_cjs(&alloc2, &owned); + let expr = first_expression(&ret2.program).unwrap(); + let out = resolve_child_node( + expr, + &owned, + &scope, + code, + &aliases, + None, + &HelperIndex::default(), + 0, + Some(ret_at), + ); + match out.as_slice() { + [root] if root.tag == "iq" => root.children.clone(), + _ => out, + } + } + + #[test] + fn a_siblings_local_cannot_donate_an_argument_path() { + // The minifier reuses `a` in every builder of a module and the variable scope is + // flat and name-keyed, so the sibling declared FIRST is the first candidate for + // the name. Only an initializer written inside the enclosing function may name a + // path, or `` here is addressed as `wrongArgs` — the shape that had + // `GroupsCreate`'s `` reading out of `participantArgs`. + let code = r#" + function sibling(e){ var a = e.wrongArgs; return o("WASmaxJsx").smax("x", null); } + function t(e){ return o("WASmaxJsx").smax("other", null); } + function b(e){ var a = e.rightArgs; + return o("WASmaxJsx").smax("iq", null, o("WASmaxChildren").OPTIONAL_CHILD(t, a)); } + "#; + let out = resolve_in_module(code, "b"); + let n = out.iter().find(|c| c.tag == "other").expect("child"); + assert_eq!(path_of(&n.arg_path), vec![("rightArgs".to_string(), false)]); + } + + #[test] + fn an_arrow_records_its_argument_root_like_a_function() { + // The two spellings of "takes one argument object" must agree, or a path that is + // recoverable in one is silently absent in the other. Nothing in today's bundle + // takes the arrow form (it is down-levelled to ES5), so this pins the symmetry + // rather than covering live output. + // + // Scope-level rather than end-to-end on purpose: an arrow passed as a child + // TEMPLATE is still not traced, because `resolve_template_arg` reaches a template + // through `VarInit::fn_body`, which only a `function` expression sets. That is a + // separate, pre-existing limit of template resolution — asserting it here would + // claim a coverage this change does not deliver. + let code = r#"var t = (e) => o("WASmaxJsx").smax("x", {jid: e.participantJid});"#; + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, code); + let scope = build_var_scope(&ret.program); + let inside = code.find("e.participantJid").expect("marker"); + assert_eq!( + scope.arg_root_at(inside).map(|(_, _, name)| name), + Some("e"), + "an arrow's single parameter is an argument root" + ); + } + + #[test] + fn an_ambiguous_source_yields_no_argument_path() { + // Two rules, one reason, so they are asserted together: a call reading two + // arguments (``) and a local assigned from two different ones (``) each + // have no single address. Publishing the first would be right about half the + // time, and a consumer following it writes into the wrong argument with nothing + // to warn it — worse than publishing nothing, which is counted. + let code = r#" + function b(e){ var x = e.old; x = e.current; + return o("WASmaxJsx").smax("iq", null, [ + o("WASmaxJsx").smax("a", {v: combine(e.left, e.right)}), + o("WASmaxJsx").smax("b", {v: o("WAWap").CUSTOM_STRING(x)})]); } + "#; + let out = resolve_builder(code, "b"); + for tag in ["a", "b"] { + let n = out.iter().find(|c| c.tag == tag).expect(tag); + assert!(n.attrs[0].arg_path.is_none(), "<{tag}>: {:?}", n.attrs[0]); + } + } + + #[test] + fn a_repeated_assignment_of_the_same_argument_is_not_ambiguous() { + // The bound is on DISAGREEMENT, not on multiplicity: two initializers naming the + // same argument still name one address, and dropping those would lose paths the + // builder states plainly. + let code = r#" + function b(e){ var x = e.jid; x = e.jid; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("u", {v: o("WAWap").JID(x)})); } + "#; + let out = resolve_builder(code, "b"); + let u = out.iter().find(|c| c.tag == "u").expect("u"); + assert_eq!( + path_of(&u.attrs[0].arg_path), + vec![("jid".to_string(), false)] + ); + } + + #[test] + fn a_nested_helpers_local_does_not_shadow_its_parents() { + // A nested function's body lies inside its parent's byte range, so a range test + // alone counts `h`'s `var x` as an initializer of the parent's `x`. Under the + // ambiguity rule that does not merely pick wrong — it sees two disagreeing + // sources and drops the parent's perfectly good path. + let code = r#" + function b(e){ var x = e.outer; + function h(e){ var x = e.inner; return o("WASmaxJsx").smax("inner", {v: x}); } + return o("WASmaxJsx").smax("iq", null, o("WASmaxJsx").smax("outer", {v: x})); } + "#; + let out = resolve_builder(code, "b"); + let n = out.iter().find(|c| c.tag == "outer").expect("outer"); + assert_eq!( + path_of(&n.attrs[0].arg_path), + vec![("outer".to_string(), false)] + ); + } + + /// A one-attribute `` whose `jid` sits at `userJid` relative to the helper that + /// built it — the shape the cross-module index stores. + fn indexed_user_helper() -> HelperIndex { + let user = WapChildNode { + tag: "user".to_string(), + attrs: vec![WapAttrDef { + name: "jid".to_string(), + kind: WapAttrKind::UserJid, + value: None, + required: true, + enum_ref: None, + arg_path: Some(vec![WapArgSegment { + key: "userJid".to_string(), + list: false, + }]), + }], + ..Default::default() + }; + HelperIndex::with("WAWebUserApi", "userNode", vec![user]) + } + + #[test] + fn a_cross_module_helper_handed_the_whole_argument_object_keeps_its_paths() { + // `helper(e)` passes the caller's own argument object, so the helper's frame IS + // the caller's and the right prefix is nothing. An empty prefix is a RESULT, and + // reading it as a failure cleared paths that were already absolute and correct. + let code = r#" + function b(e){ return o("WASmaxJsx").smax("iq", null, o("WAWebUserApi").userNode(e)); } + "#; + let out = resolve_builder_with(code, "b", &indexed_user_helper()); + let u = out.iter().find(|c| c.tag == "user").expect("user"); + assert_eq!( + path_of(&u.attrs[0].arg_path), + vec![("userJid".to_string(), false)], + "no prefix needed, and none applied" + ); + } + + #[test] + fn a_cross_module_helper_handed_a_keyed_argument_is_prefixed() { + let code = r#" + function b(e){ var a = e.userArgs; + return o("WASmaxJsx").smax("iq", null, o("WAWebUserApi").userNode(a)); } + "#; + let out = resolve_builder_with(code, "b", &indexed_user_helper()); + let u = out.iter().find(|c| c.tag == "user").expect("user"); + assert_eq!( + path_of(&u.attrs[0].arg_path), + vec![ + ("userArgs".to_string(), false), + ("userJid".to_string(), false) + ] + ); + } + + #[test] + fn a_cross_module_helper_handed_something_unnameable_keeps_nothing() { + // The index is built with no call site in view, so nothing downstream will ever + // rebase this subtree. A path relative to a frame nobody can name is a wrong + // address, and a wrong address is worse than an absent one. + let code = r#" + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WAWebUserApi").userNode({jid: e.userJid})); } + "#; + let out = resolve_builder_with(code, "b", &indexed_user_helper()); + let u = out.iter().find(|c| c.tag == "user").expect("user"); + assert!(u.attrs[0].arg_path.is_none(), "{:?}", u.attrs[0]); + } + + #[test] + fn a_conditional_arm_that_reads_an_argument_makes_it_ambiguous() { + // `cond ? e.primary : e.values[i]` — the second arm has no static key, so the + // resolver cannot spell it out, but it plainly READS an argument and may be the + // one the builder uses. Publishing the arm that resolved points a consumer at a + // source that is only sometimes right. + let code = r#" + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("x", {v: e.flag ? e.primary : e.values[i]})); } + "#; + let out = resolve_builder(code, "b"); + let n = out.iter().find(|c| c.tag == "x").expect("x"); + assert!(n.attrs[0].arg_path.is_none(), "{:?}", n.attrs[0]); + } + + #[test] + fn a_conditional_against_a_constant_keeps_the_resolved_arm() { + // The shape WA actually writes: a value or `DROP_ATTR`. The alternate names no + // argument at all, so it is not a second source and the one path stands. + let code = r#" + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("x", {v: e.kind === "s" ? o("WAWap").CUSTOM_STRING(e.stamp) : o("WAWap").DROP_ATTR})); } + "#; + let out = resolve_builder(code, "b"); + let n = out.iter().find(|c| c.tag == "x").expect("x"); + assert_eq!( + path_of(&n.attrs[0].arg_path), + vec![("stamp".to_string(), false)] + ); + } + + #[test] + fn a_destructured_parameter_does_not_shrink_the_arity() { + // `function f(e, {x})` has two formals. Skipping the pattern would leave one name + // behind and make `e` look like the sole options object, publishing its keys as + // addresses into an argument list that has two positions. + let code = r#" + function b(e, {x}){ return o("WAWap").wap("iq", null, + o("WAWap").wap("u", {jid: e.jid})); } + "#; + let out = resolve_builder(code, "b"); + let u = out.iter().find(|c| c.tag == "u").expect("u"); + assert!(u.attrs[0].arg_path.is_none(), "{:?}", u.attrs[0]); + } + + #[test] + fn an_optional_child_handed_something_unnameable_keeps_nothing() { + // The template's paths are relative to the object literal, which this frame + // cannot name. Left alone they read as addresses into the request's own + // arguments — `jid` rather than `userArgs → jid`. + let code = r#" + function t(e){ return o("WASmaxJsx").smax("u", {jid: o("WAWap").JID(e.jid)}); } + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").OPTIONAL_CHILD(t, {jid: e.userJid})); } + "#; + let out = resolve_builder(code, "b"); + let u = out.iter().find(|c| c.tag == "u").expect("u"); + assert!( + u.arg_path.is_none() && u.attrs[0].arg_path.is_none(), + "{u:?}" + ); + } + + #[test] + fn a_wrapper_around_a_mapped_node_carries_no_payload() { + // `wap("product", null, wap("id", null, v))` over a list of scalars: the value is + // the inner element's, and the wrapper has none. The flat sweep that collects a + // mapper's calls does not nest them, so nothing else stops the wrapper from + // resolving its child argument as a payload — it would report `productIds[]` as + // its own content and a consumer would write the id twice, once where it does not + // belong. + let code = r#" + function b(e){ var l = e.productIds; + return o("WAWap").wap("iq", null, o("WAWap").wap("product_list", null, + l.map(function(v){ return o("WAWap").wap("product", null, o("WAWap").wap("id", null, v)); }))); } + "#; + let out = resolve_builder(code, "b"); + let all: Vec<_> = out + .iter() + .flat_map(|c| std::iter::once(c).chain(c.children.iter())) + .collect(); + let product = all.iter().find(|c| c.tag == "product").expect("product"); + let id = all.iter().find(|c| c.tag == "id").expect("id"); + assert!( + product.content.is_none(), + "the wrapper holds a node, not a value: {:?}", + product.content + ); + assert_eq!( + path_of( + &id.content + .as_ref() + .expect("the id carries the value") + .arg_path + ), + vec![("productIds".to_string(), true)] + ); + } + + #[test] + fn a_map_mapper_is_rebased_onto_its_receiver() { + // `.map()` reads one element of the receiver, so the receiver is the list and the + // mapper's keys hang off an element — the same shape as `REPEATED_CHILD`. + let code = r#" + function b(e){ var l = e.productIds; + return o("WAWap").wap("iq", null, o("WAWap").wap("list", null, + l.map(function(e){ return o("WAWap").wap("product", {id: e.productId}); }))); } + "#; + let out = resolve_builder(code, "b"); + let p = out + .iter() + .flat_map(|c| std::iter::once(c).chain(c.children.iter())) + .find(|c| c.tag == "product") + .expect("product"); + assert!(p.repeats); + assert_eq!( + path_of(&p.arg_path), + vec![("productIds".to_string(), true)], + "the node addresses the list it is mapped over" + ); + assert_eq!( + path_of(&p.attrs[0].arg_path), + vec![ + ("productIds".to_string(), true), + ("productId".to_string(), false) + ], + "and its keys are read off an element, not off the request's arguments" + ); + } + + #[test] + fn a_callback_parameter_shadows_a_module_alias() { + // The other half of layering the module's aliases into a callback body: the + // minifier spells everything `n`/`t`/`a`, so a callback whose parameter collides + // with the module's `WASmaxAttrs` alias would have its own element read as that + // builder — `A.OPTIONAL_LITERAL(…)` on an element `A` is a property of the + // element, not a fixed wire literal. Fifteen such collisions exist in the current + // bundle. + let code = r#" + var A; A = o("WASmaxAttrs"); + function b(e){ var l = e.itemArgs; + return o("WAWap").wap("iq", null, l.map(function(A){ + return o("WAWap").wap("item", {del: A.OPTIONAL_LITERAL("true", A.hasDel)}); })); } + "#; + let out = resolve_builder(code, "b"); + let item = out + .iter() + .flat_map(|c| std::iter::once(c).chain(c.children.iter())) + .find(|c| c.tag == "item") + .expect("item"); + let del = item.attrs.iter().find(|a| a.name == "del").expect("del"); + assert!( + del.value.is_none(), + "a method on the ELEMENT is not `WASmaxAttrs.OPTIONAL_LITERAL`, so there is no + fixed wire value to record: {del:?}" + ); + assert_eq!( + path_of(&del.arg_path), + vec![ + ("itemArgs".to_string(), true), + ("hasDel".to_string(), false) + ], + "and what it reads is a key off that element" + ); + } + + #[test] + fn a_nested_functions_parameter_does_not_shadow_the_body() { + // Shadowing is per scope. A helper declared inside the callback binds its own + // parameter, not the callback's — so an outer `A = o("WASmaxAttrs")` is still the + // builder for the callback's own calls. Treating every binding anywhere in the + // body as a shadow unresolves aliases that are perfectly in scope, which reads a + // fixed wire literal as a caller-supplied value. + let code = r#" + var A; A = o("WASmaxAttrs"); + function b(e){ var l = e.itemArgs; + return o("WAWap").wap("iq", null, l.map(function(n){ + function h(A){ return A; } + return o("WAWap").wap("item", {del: A.OPTIONAL_LITERAL("true", n.hasDel)}); })); } + "#; + let out = resolve_builder(code, "b"); + let item = out + .iter() + .flat_map(|c| std::iter::once(c).chain(c.children.iter())) + .find(|c| c.tag == "item") + .expect("item"); + let del = item.attrs.iter().find(|a| a.name == "del").expect("del"); + assert_eq!(del.kind, WapAttrKind::Optional); + assert_eq!(del.value.as_deref(), Some("true"), "{del:?}"); + } + + #[test] + fn a_map_callback_reads_the_enclosing_module_aliases() { + // The callback body is re-parsed on its own, so the module-level + // `A = o("WASmaxAttrs")` it calls through is not declared in what the sweep sees. + // With only the body's own aliases, `A.OPTIONAL_LITERAL("true", e.hasDelete)` is + // an ordinary dynamic value: the fixed wire string is lost and the boolean that + // merely gates the attribute is published as the address to put a value at. + let code = r#" + var A; A = o("WASmaxAttrs"); + function b(e){ var l = e.itemArgs; + return o("WAWap").wap("iq", null, l.map(function(e){ + return o("WAWap").wap("item", {del: A.OPTIONAL_LITERAL("true", e.hasDelete)}); })); } + "#; + let out = resolve_builder(code, "b"); + let item = out + .iter() + .flat_map(|c| std::iter::once(c).chain(c.children.iter())) + .find(|c| c.tag == "item") + .expect("item"); + let del = item.attrs.iter().find(|a| a.name == "del").expect("del"); + assert_eq!(del.kind, WapAttrKind::Optional); + assert_eq!(del.value.as_deref(), Some("true")); + assert!( + del.arg_path.is_none(), + "a fixed literal's gate is not its value address: {:?}", + del.arg_path + ); + } + + #[test] + fn a_mixin_handed_the_whole_argument_object_keeps_its_paths() { + // `OPTIONAL_CHILD(t, e)` / `merge…Mixin(dst, e)` hand over the caller's own + // argument object, so the callee's frame IS the caller's and the right prefix is + // nothing. Collapsing that into the same "cannot name it" answer clears paths + // that were already absolute — the resolved address is thrown away at the one + // call site that needed no work. + let code = r#" + function t(a){ return o("WASmaxJsx").smax("u", {jid: o("WAWap").JID(a.userJid)}); } + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").OPTIONAL_CHILD(t, e)); } + "#; + let out = resolve_builder(code, "b"); + let u = out.iter().find(|c| c.tag == "u").expect("u"); + assert_eq!( + path_of(&u.attrs[0].arg_path), + vec![("userJid".to_string(), false)], + "already absolute, so nothing to prefix and nothing to clear" + ); + } + + #[test] + fn a_scalar_list_template_keeps_the_element_as_its_content() { + // A template whose whole parameter is the payload — a list of scalars, not of + // argument objects. Its relative path is empty, which is a real answer ("the + // element itself"): the combinator rebases it onto the list, giving a content + // addressed at `productIds[]`. Discarding the empty path left the node with no + // content at all, before anything could rebase it. + let code = r#" + function t(v){ return o("WAWap").wap("id", null, v); } + function b(e){ return o("WAWap").wap("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, e.productIds, 0, 1/0)); } + "#; + let out = resolve_builder(code, "b"); + let id = out.iter().find(|c| c.tag == "id").expect("id"); + let content = id.content.as_ref().expect("the element carries its value"); + assert_eq!( + path_of(&content.arg_path), + vec![("productIds".to_string(), true)], + "the value IS the element of the list" + ); + } + + #[test] + fn an_empty_path_is_never_published() { + // The empty path composes; it does not publish. Anything still empty where a + // request is assembled was prefixed by nobody, and "the argument object itself" + // is not an address a consumer writes a value at. + let mut nodes = vec![WapChildNode { + tag: "id".to_string(), + content: Some(WapContent { + kind: WapContentKind::Dynamic, + arg_path: Some(Vec::new()), + ..Default::default() + }), + arg_path: Some(Vec::new()), + ..Default::default() + }]; + drop_empty_arg_paths(&mut nodes); + assert!(nodes[0].arg_path.is_none()); + assert!(nodes[0].content.as_ref().unwrap().arg_path.is_none()); + } + + #[test] + fn a_mixin_frame_names_the_argument_it_dispatches_on() { + // WA's mixins take `(dst, args)`, and a MixinGroup dispatches on a key of its own + // `args` — `if (t.setConfig) return mergeSetConfigMixin(e, t.setConfig)`. Under + // the builder rule that frame has no argument object, so the hop is unnameable + // and everything the branch contributes loses its address. The merge rule reads + // the second formal, which is what WA's signature means. + let code = r#" + function m(e,t){ if (t.setConfig) return o("WASmaxOutX").mergeXMixin(e, t.setConfig); + return e; } + "#; + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, code); + let scope = build_var_scope(&ret.program); + let at = code.find("mergeXMixin(").expect("the merge call"); + let alloc2 = Allocator::default(); + let owned = format!("{};", &code[at..code[at..].find(')').unwrap() + at + 1]); + let ret2 = wa_oxc::parse_cjs(&alloc2, &owned); + let call = first_expression(&ret2.program) + .and_then(as_call) + .expect("a call"); + assert_eq!( + MergeArg::of_in_mixin(call, &scope, code, Some(at)), + MergeArg::Prefix(vec![WapArgSegment { + key: "setConfig".to_string(), + list: false + }]) + ); + assert_eq!( + MergeArg::of(call, &scope, code, Some(at)), + MergeArg::Unnameable, + "the builder rule still refuses a two-parameter frame" + ); + } + + #[test] + fn merge_arguments_compose_along_a_chain() { + let key = |k: &str| WapArgSegment { + key: k.to_string(), + list: false, + }; + let a = MergeArg::Prefix(vec![key("groupArgs")]); + let b = MergeArg::Prefix(vec![key("subject")]); + assert_eq!( + a.then(&b), + MergeArg::Prefix(vec![key("groupArgs"), key("subject")]) + ); + // Whole is the identity — the ordinary case, where a mixin is handed the whole + // argument object and its paths are already the request's. + assert_eq!(a.then(&MergeArg::Whole), a); + assert_eq!(MergeArg::Whole.then(&b), b); + // One hop nobody can name makes the rest of the chain unnameable too. + assert_eq!(a.then(&MergeArg::Unnameable), MergeArg::Unnameable); + assert_eq!(MergeArg::Unnameable.then(&b), MergeArg::Unnameable); + } + + #[test] + fn a_key_read_off_a_call_result_is_not_an_argument_path() { + // `attrFromReference(accessor, e, ["id"])` reads out of the stanza being ACKED. + // The general walk descends into a call's arguments, finds the bare parameter and + // reports the empty path, which then absorbs `.value` — publishing `value` as an + // address into the builder's options object, which it is not. A key read off a + // computed value is not an address however the value was derived. + let code = r#" + function b(e){ var t = o("M").attrFromReference(o("P").attrStanzaId, e, ["id"]); + return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("ack", {id: o("WAWap").STANZA_ID(t.value)})); } + "#; + let out = resolve_builder(code, "b"); + let ack = out.iter().find(|c| c.tag == "ack").expect("ack"); + assert!(ack.attrs[0].arg_path.is_none(), "{:?}", ack.attrs[0]); + } + + #[test] + fn a_key_read_off_the_parameter_is_still_an_argument_path() { + // The counterpart, so the restriction above stays a restriction on CALLS rather + // than on member chains: a nested read off the parameter is a plain address. + let code = r#" + function b(e){ var t = e.outer; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("x", {v: o("WAWap").CUSTOM_STRING(t.inner)})); } + "#; + let out = resolve_builder(code, "b"); + let x = out.iter().find(|c| c.tag == "x").expect("x"); + assert_eq!( + path_of(&x.attrs[0].arg_path), + vec![("outer".to_string(), false), ("inner".to_string(), false)] + ); + } + + #[test] + fn a_computed_minimum_suppresses_both_bounds() { + // Bounds move together. A ceiling with no floor is half a range, and it would be + // counted as a recovered bound while the floor the builder also enforces is + // silently absent — the same conflation the computed-maximum rule avoids. + let code = r#" + function t(e){ return o("WASmaxJsx").smax("item", null); } + function b(e){ var a = e.itemArgs, n = e.floor; + return o("WASmaxJsx").smax("iq", null, + o("WASmaxChildren").REPEATED_CHILD(t, a, n, 10)); } + "#; + let out = resolve_builder(code, "b"); + let item = out.iter().find(|c| c.tag == "item").expect("item"); + assert_eq!((item.repeat_min, item.repeat_max), (None, None)); + } + + #[test] + fn an_optional_literal_attribute_names_no_value_path() { + // `OPTIONAL_LITERAL("true", flag)` writes a FIXED wire value and takes a boolean + // deciding whether to write it. Reporting the boolean as the value's address + // tells a consumer to put the wire string there. + let code = r#" + function b(e){ return o("WASmaxJsx").smax("iq", null, + o("WASmaxJsx").smax("description", { + id: o("WAWap").CUSTOM_STRING(e.descriptionId), + delete: o("WASmaxAttrs").OPTIONAL_LITERAL("true", e.hasDelete)})); } + "#; + let out = resolve_builder(code, "b"); + let d = out + .iter() + .find(|c| c.tag == "description") + .expect("description"); + let by = |n: &str| d.attrs.iter().find(|a| a.name == n).expect(n); + assert_eq!( + path_of(&by("id").arg_path), + vec![("descriptionId".to_string(), false)], + "an ordinary optional attribute still names its value" + ); + assert!(by("delete").arg_path.is_none(), "{:?}", by("delete")); + } + + #[test] + fn an_optional_merge_introducing_a_tag_marks_it_optional() { + // `optionalMerge` can skip the whole contribution, so a tag it INTRODUCES may be + // absent from the request. A tag that merges onto a destination built at its own + // call site keeps that site's cardinality instead. + let mut dst = vec![cnode("create", &[])]; + apply_contribution(&mut dst, &[cnode("create", &[]), cnode("extra", &[])], true); + let by = |t: &str| { + dst.iter() + .find(|c| c.tag == t) + .unwrap_or_else(|| panic!("{t}: {dst:?}")) + .presence + }; + assert_eq!( + by("extra"), + WapChildPresence::Optional, + "a tag the optional mixin introduces" + ); + assert_eq!( + by("create"), + WapChildPresence::Required, + "a tag that already existed keeps its own call site's cardinality" + ); + } + + #[test] + fn a_multi_parameter_builder_yields_no_argument_path() { + // A legacy `WAWeb*Job` builder takes positional parameters, so a bare key would + // name a property without naming which parameter it hangs off. That reads like + // an address and is not one, so nothing is emitted. + let code = r#" + function b(e, t){ var j = e.jid; + return o("WAWap").wap("iq", null, o("WAWap").wap("user", {jid: j})); } + "#; + let out = resolve_builder(code, "b"); + let u = out.iter().find(|c| c.tag == "user").expect("user"); + assert!(u.attrs[0].arg_path.is_none(), "{:?}", u.attrs[0]); + } + + /// Like [`resolve`], but with cross-module mixin contributions available (Phase + /// 3): a `merge…Mixin(dst,…)` folds the mixin's attrs/children into `dst`. + fn resolve_with( + code: &str, + expr_src: &str, + contributions: &MixinContributions, + helpers: &HelperIndex, + ) -> Vec { + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, code); + let scope = build_var_scope(&ret.program); + let aliases = build_alias_map(&ret.program); + let alloc2 = Allocator::default(); let owned = format!("{expr_src};"); let ret2 = wa_oxc::parse_cjs(&alloc2, &owned); @@ -1426,11 +3397,13 @@ mod tests { value: None, required: false, enum_ref: None, + arg_path: None, }) .collect(), children: Vec::new(), repeats: false, variant_groups: Vec::new(), + ..Default::default() } } @@ -1688,7 +3661,7 @@ mod tests { #[test] fn find_wap_calls_collects_flat() { let body = r#"{ var a = e.wap("one", {}); foo(); return e.wap("two", {z:"9"}); }"#; - let out = find_wap_calls_in_body(body, &AliasMap::default()); + let out = find_wap_calls_in_body(body, &AliasMap::default(), None); let tags: Vec<_> = out.iter().map(|c| c.tag.as_str()).collect(); assert_eq!(tags, ["one", "two"]); assert!(out.iter().all(|c| !c.repeats && c.children.is_empty())); @@ -1734,6 +3707,7 @@ mod tests { content: None, repeats: false, variant_groups: vec![], + ..Default::default() }; let src = WapChildNode { tag: "item".into(), @@ -1742,6 +3716,7 @@ mod tests { content: None, repeats: true, variant_groups: vec![], + ..Default::default() }; merge_node_into(&mut dst, &src, false); assert!( diff --git a/crates/wa-scan/src/stanza.rs b/crates/wa-scan/src/stanza.rs index 42c0939..55c11a8 100644 --- a/crates/wa-scan/src/stanza.rs +++ b/crates/wa-scan/src/stanza.rs @@ -22,7 +22,10 @@ use crate::alias::{AliasMap, build_alias_map}; use crate::attrs::{extract_attrs_from_obj, parse_wap_call}; use crate::enum_link::EnumResolver; use crate::helper_index::HelperIndex; -use crate::request::{VarScope, build_var_scope, resolve_child_node}; +use crate::request::{ + VarScope, annotate_attr_arg_paths, build_var_scope, enforce_argument_boundary, + resolve_child_node, +}; /// The outgoing stanzas this scanner recognizes, by top-level tag. IQ has its own /// path; the incoming dispatch side comes later. @@ -219,10 +222,25 @@ impl<'a> Visit<'a> for StanzaCollector<'_> { if let Some(wap) = parse_wap_call(call, self.aliases) && let Some(stanza_type) = stanza_tag(wap.tag) { - let attrs = wap + let mut attrs = wap .attrs_node .map(|n| extract_attrs_from_obj(n, self.source, self.aliases)) .unwrap_or_default(); + // The root's own attributes are argument-backed too — `` is most of what that builder writes — and unlike ``, whose + // root attrs are consumed into `namespace`/`iqType`/`target`, they are + // published as attributes. Without this they were the one place a consumer + // could see a dynamic value and not where to supply it. + if let Some(n) = wap.attrs_node { + annotate_attr_arg_paths( + &mut attrs, + n, + self.aliases, + self.scope, + self.source, + Some(n.span().start as usize), + ); + } // The `type` attr distinguishes stanza subtypes (`receipt type="read"`, …). let subtype = attrs .iter() @@ -246,6 +264,10 @@ impl<'a> Visit<'a> for StanzaCollector<'_> { )); } } + // See `try_iq_call`: a builder with no single options object publishes no + // paths, inlined subtrees included. Once for the whole call — every child + // shares the frame that built it. + enforce_argument_boundary(&mut children, self.scope, call.span().start as usize); self.out.push(StanzaDef { stanza_type, direction: Direction::Outgoing, diff --git a/crates/wa-scan/tests/iq_builder_contract.rs b/crates/wa-scan/tests/iq_builder_contract.rs new file mode 100644 index 0000000..ea83a6b --- /dev/null +++ b/crates/wa-scan/tests/iq_builder_contract.rs @@ -0,0 +1,384 @@ +//! Guard: the committed IQ IR describes WA's **builder**, not only the wire. +//! +//! There are two shapes of consumer. One encodes the stanza itself and needs to know +//! that a group create carries a ``; the IR has always said that. +//! The other runs WA's own module and needs the other half — that the value goes in +//! `args.participantArgs[].participantJid` — which no amount of wire description +//! implies, because the argument key and the attribute name are chosen independently +//! (`subjectElementValue` becomes the text of ``). +//! +//! Everything here is checked against fixtures named by RPC rather than by count, so a +//! WA refactor that moves a construct fails on the construct instead of on an integer. +//! The aggregate coverage numbers live in `manifest.diagnostics.iq.builder` and are +//! floored by the update guard; the point of this file is the specific facts a consumer +//! would build a code generator on. + +use std::path::Path; + +use wa_ir::{IqIr, IqStanzaDef, ParsedField, WapArgSegment, WapChildNode, WapChildPresence}; + +/// Load the committed IR, or skip locally when it is absent (sparse checkout) while +/// still failing under CI, where its absence would silently disable every guard here. +/// Mirrors the committed-artifact gate in `iq_roundtrip.rs`. +fn committed_ir() -> Option { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../generated/iq/index.json"); + if !path.exists() { + assert!( + std::env::var_os("CI").is_none(), + "{} is absent under CI — the builder-contract guard would be silently skipped", + path.display() + ); + eprintln!("skipping: {} not present (local only)", path.display()); + return None; + } + let raw = std::fs::read_to_string(&path).expect("read generated/iq/index.json"); + Some(serde_json::from_str(&raw).expect("parse the committed IQ IR")) +} + +fn stanza<'a>(ir: &'a IqIr, module: &str) -> &'a IqStanzaDef { + ir.stanzas + .iter() + .find(|s| s.module_name == module) + .unwrap_or_else(|| panic!("{module} vanished from the IR")) +} + +/// Depth-first over a request subtree, including variant-group children. +fn nodes(node: &WapChildNode) -> Vec<&WapChildNode> { + let mut out = vec![node]; + for c in &node.children { + out.extend(nodes(c)); + } + for g in &node.variant_groups { + for v in &g.variants { + for c in &v.children { + out.extend(nodes(c)); + } + } + } + out +} + +fn request_nodes(s: &IqStanzaDef) -> Vec<&WapChildNode> { + s.request.children.iter().flat_map(nodes).collect() +} + +fn find<'a>(s: &'a IqStanzaDef, tag: &str) -> &'a WapChildNode { + request_nodes(s) + .into_iter() + .find(|n| n.tag == tag) + .unwrap_or_else(|| panic!("<{tag}> vanished from {}", s.module_name)) +} + +fn path(segments: &[WapArgSegment]) -> Vec<(&str, bool)> { + segments.iter().map(|s| (s.key.as_str(), s.list)).collect() +} + +#[test] +fn a_group_rename_carries_the_name() { + // The narrowest possible statement of the element-value gap: `SetSubject`'s entire + // payload is the text of ``, and the builder reads it out of the argument + // object before writing it. Without the content the IR describes a request that + // renames a group to nothing. + let Some(ir) = committed_ir() else { return }; + let s = stanza(&ir, "WASmaxOutGroupsSetSubjectRequest"); + let subject = find(s, "subject"); + let content = subject + .content + .as_ref() + .expect("`` carries its element value"); + assert_eq!( + path(content.arg_path.as_deref().unwrap_or_default()), + vec![("subjectElementValue", false)], + "and says which argument fills it" + ); +} + +#[test] +fn a_repeated_child_is_indexed_and_an_optional_one_is_not() { + // The two live side by side in one builder, which is why they are asserted + // together: `REPEATED_CHILD` runs its template once per element, `OPTIONAL_CHILD` + // hands the object over whole. Reading one as the other writes the value somewhere + // the vendor builder never looks, and the stanza goes out missing it. + let Some(ir) = committed_ir() else { return }; + let s = stanza(&ir, "WASmaxOutGroupsCreateRequest"); + + let participant = find(s, "participant"); + assert!(participant.repeats); + assert_eq!( + path(participant.arg_path.as_deref().unwrap_or_default()), + vec![("participantArgs", true)] + ); + let jid = participant + .attrs + .iter() + .find(|a| a.name == "jid") + .expect("participant jid"); + assert_eq!( + path(jid.arg_path.as_deref().unwrap_or_default()), + vec![("participantArgs", true), ("participantJid", false)], + "the list is indexed, the key read off an element is not" + ); + + let description = find(s, "description"); + assert_eq!(description.presence, WapChildPresence::Optional); + let id = description + .attrs + .iter() + .find(|a| a.name == "id") + .expect("description id"); + assert_eq!( + path(id.arg_path.as_deref().unwrap_or_default()), + vec![("descriptionArgs", false), ("descriptionId", false)] + ); + assert!( + description + .arg_path + .iter() + .flatten() + .chain(id.arg_path.iter().flatten()) + .all(|seg| !seg.list), + "nothing under an OPTIONAL_CHILD may be indexed" + ); +} + +#[test] +fn the_three_cardinalities_are_distinguishable_on_a_real_request() { + // ``, `` and `` are three children of one + // ``. On the wire the first is an empty element; in the IR it used to be + // the same `{tag, [], []}` as a required empty one, so a consumer had no way to + // learn it is the `bool` argument `hasLocked`. + let Some(ir) = committed_ir() else { return }; + let s = stanza(&ir, "WASmaxOutGroupsCreateRequest"); + + let locked = request_nodes(s) + .into_iter() + .find(|n| n.tag == "locked" && n.presence == WapChildPresence::PresenceFlag) + .expect(" is a presence marker"); + assert!(locked.attrs.is_empty() && locked.content.is_none()); + assert_eq!( + path(locked.arg_path.as_deref().unwrap_or_default()), + vec![("hasLocked", false)], + "a marker addresses its boolean, not an argument object" + ); + + assert_eq!(find(s, "description").presence, WapChildPresence::Optional); + assert_eq!(find(s, "participant").presence, WapChildPresence::Required); +} + +#[test] +fn repeat_bounds_are_carried_where_the_builder_states_them() { + // The server enforces these; a consumer that generates a plain `Vec` finds the + // ceiling by being rejected. Named RPCs rather than a count, so a WA change to one + // limit is reported as that limit rather than as a smaller total. + let Some(ir) = committed_ir() else { return }; + let bounded: Vec<_> = ir + .stanzas + .iter() + .flat_map(request_nodes) + .filter(|n| n.repeats) + .filter_map(|n| Some((n.tag.as_str(), n.repeat_min?, n.repeat_max?))) + .collect(); + for expected in [ + ("participant", 1u32, 1024u32), + ("participant", 1, 19999), + ("group", 1, 10000), + ("media_list", 0, 10), + ] { + assert!( + bounded.contains(&expected), + "{expected:?} is no longer stated; have {bounded:?}" + ); + } + // An unbounded maximum (`1/0` in the bundle) keeps its minimum, which is what makes + // "at least n, no ceiling" distinguishable from a child that states no bound at all. + assert!( + ir.stanzas + .iter() + .flat_map(request_nodes) + .any(|n| n.repeats && n.repeat_min.is_some() && n.repeat_max.is_none()), + "no explicitly unbounded repeated child left" + ); +} + +#[test] +fn every_combinator_child_of_a_smax_builder_is_addressable() { + // The coverage claim, stated where it is true rather than globally: every child a + // `WASmaxOut*Request` builds through a `WASmaxChildren` combinator, and every + // attribute and content under one, knows which argument fills it. + // + // The legacy `WAWeb*Job` builders are deliberately out: they take positional + // parameters, so a bare key would name a property without naming which parameter it + // hangs off. They are counted in `manifest.diagnostics.iq.builder`, not addressed. + let Some(ir) = committed_ir() else { return }; + let mut missing = Vec::new(); + for s in ir + .stanzas + .iter() + .filter(|s| s.module_name.starts_with("WASmaxOut") && s.module_name.ends_with("Request")) + { + for n in request_nodes(s) { + if n.is_combinator_fed() && n.arg_path.is_none() { + missing.push(format!("{}: <{}> node", s.module_name, n.tag)); + } + // Variant-group attributes included. Walking only `n.attrs` made this pass + // by not looking: the seven unaddressed attributes below all live inside a + // disjunction, and the claim read as though it covered them. + let variant_attrs = n + .variant_groups + .iter() + .flat_map(|g| &g.variants) + .flat_map(|v| &v.attrs); + for a in n.attrs.iter().chain(variant_attrs) { + // `reads_argument`/`is_combinator_fed` are the IR's own predicates, so + // this test and the `diagnostics.iq.builder` counters cannot drift into + // measuring different things — one rule written twice is the defect this + // batch keeps finding. + if a.reads_argument() && a.arg_path.is_none() { + missing.push(format!("{}: <{}> @{}", s.module_name, n.tag, a.name)); + } + } + if let Some(c) = &n.content + && c.reads_argument() + && c.arg_path.is_none() + { + missing.push(format!("{}: <{}> content", s.module_name, n.tag)); + } + } + } + // No exemption. `PushConfigSet` used to be one — it hands its mixin a sub-object + // rather than the whole argument object, and the by-module-name mixin closure had no + // call site to rebase what that returned. The closure now carries each merge call's + // argument, so those attributes are addressed like every other. + assert!(missing.is_empty(), "unaddressable: {missing:?}"); +} + +#[test] +fn a_runtime_addressee_says_which_argument_supplies_it() { + // `target` says this request goes to one group's own JID rather than to a server. + // That is half an instruction: a consumer running the vendor builder still has to know + // where the JID goes. Both are needed, and the second is recoverable — including + // through a mixin group, where the path composes across the fold. + let Some(ir) = committed_ir() else { return }; + let path_of = |name: &str| -> Option> { + ir.stanzas + .iter() + .find(|s| s.module_name == name) + .and_then(|s| s.request.target_arg_path.as_ref()) + .map(|p| p.iter().map(|x| x.key.clone()).collect()) + }; + assert_eq!( + path_of("WASmaxOutGroupsGetGroupInfoRequest").as_deref(), + Some(["iqTo".to_string()].as_slice()), + "a group request addresses the group it is about" + ); + // And a request whose addressee comes from a runtime ROUTER names none. Its two arms + // address a group's own JID and the group server; the union already reports `unknown` + // for which, and one arm's key is not the request's answer. The composition works — + // the path resolves to `baseGetGroupOrServerMixinGroupArgs → baseGetGroup → iqTo` + // before the disagreement withdraws it — which is why the withdrawal is deliberate + // rather than a gap. + assert!( + path_of("WASmaxOutGroupsGetGroupProfilePicturesRequest").is_none(), + "a disagreement about which addressee is one about its address" + ); + // A constant addressee has nothing to supply, so it names nothing. + assert!( + path_of("WASmaxOutGroupsCreateRequest").is_none(), + "the group server is a constant, not an argument" + ); +} + +#[test] +fn a_pinned_constant_payload_keeps_its_address() { + // `` is one byte of zero at every call site, which is how + // `constBytes` got there. That does not make it builder-written: the payload is still + // read from an argument, so it keeps the address a consumer has to write that byte + // at. Dropping either fact leaves a caller unable to make the request — one without + // knowing where, the other without knowing what. + let Some(ir) = committed_ir() else { return }; + let s = ir + .stanzas + .iter() + .find(|s| s.module_name == "WASmaxOutMdCompanionHelloRequest") + .expect("the companion hello request"); + let nonce = request_nodes(s) + .into_iter() + .find(|n| n.tag == "link_code_pairing_nonce") + .expect("the nonce element"); + let c = nonce.content.as_ref().expect("content"); + assert_eq!(c.const_bytes.as_deref(), Some("00")); + assert_eq!( + c.arg_path + .as_ref() + .map(|p| p.iter().map(|s| s.key.as_str()).collect::>()), + Some(vec![ + "linkCodePairingNonceArgs", + "linkCodePairingNonceElementValue" + ]), + "pinned, not written: the argument is still there" + ); + assert!(c.reads_argument(), "and the IR's own predicate says so"); +} + +#[test] +fn a_key_bundle_fetch_descends_into_its_union() { + // `PreKeysFetchKeyBundles` is how every client gets the keys to send a first + // message, and its whole payload sits inside a `…MixinGroup` disjunction. A field + // that names a union and carries no alternatives is a leaf that looks like a + // scalar: a consumer reads a string, gets nothing, and cannot tell what it missed. + let Some(ir) = committed_ir() else { return }; + let s = stanza(&ir, "WASmaxOutPreKeysFetchKeyBundlesRequest"); + + fn fields(f: &ParsedField) -> Vec<&ParsedField> { + let mut out = vec![f]; + for c in f.children.iter().flatten() { + out.extend(fields(c)); + } + for v in f.union_variants.iter().flatten() { + for inner in &v.fields { + out.extend(fields(inner)); + } + } + out + } + let all: Vec<&ParsedField> = s + .response + .variants + .iter() + .flat_map(|v| v.fields.iter().flat_map(fields)) + .collect(); + + let group = all + .iter() + .find(|f| f.name.ends_with("MixinGroup")) + .expect("the key-bundle disjunction is still modelled"); + let arms = group + .union_variants + .as_ref() + .expect("a union names its alternatives, rather than reading as a scalar"); + assert!(arms.len() >= 2, "a union with {} arm(s)", arms.len()); + // Discriminated, not flattened: each arm keeps its own name, so a consumer generates + // a real sum type instead of a struct where every field is optional. + assert!( + arms.iter().any(|a| a.name.contains("Success")) + && arms.iter().any(|a| !a.fields.is_empty()), + "arms: {:?}", + arms.iter().map(|a| &a.name).collect::>() + ); + + // …and the invariant behind it, across every response in the IR. + for f in ir + .stanzas + .iter() + .flat_map(|s| &s.response.variants) + .flat_map(|v| v.fields.iter().flat_map(fields)) + { + assert!( + !f.name.ends_with("MixinGroup") + || f.union_variants.as_ref().is_some_and(|v| !v.is_empty()) + || f.children.as_ref().is_some_and(|c| !c.is_empty()), + "{}: a mixin group with no alternatives and no children", + f.name + ); + } +} diff --git a/crates/whatspec/src/main.rs b/crates/whatspec/src/main.rs index 0053995..9c508f1 100644 --- a/crates/whatspec/src/main.rs +++ b/crates/whatspec/src/main.rs @@ -786,6 +786,15 @@ struct Counts { iq_typed_error_variants: usize, iq_error_texts: usize, iq_error_arms: usize, + /// Builder-side coverage, mirroring `manifest.diagnostics.iq.builder` — see + /// [`IqBuilderCounts`]. + iq_arg_path_attrs: usize, + iq_arg_path_contents: usize, + iq_arg_path_children: usize, + iq_element_values: usize, + iq_optional_children: usize, + iq_presence_flag_children: usize, + iq_repeat_bounds: usize, /// Notification payload action-union arms recovered (`diagnostics.notif.actions`). notif_actions: usize, /// Shape-level coverage of those arms: resolved `actionType`s plus every field, @@ -838,6 +847,8 @@ struct IqDiagnostics { cross_module: wa_scan::CrossModuleStats, /// Validation-constraint coverage — see [`IqConstraintCounts`]. constraints: IqConstraintCounts, + /// Builder-side coverage — see [`IqBuilderCounts`]. + builder: IqBuilderCounts, /// Who the emitted requests are addressed to — see [`IqTargetCounts`]. targets: IqTargetCounts, } @@ -1000,6 +1011,134 @@ fn iq_constraint_counts(ir: &wa_ir::IqIr) -> IqConstraintCounts { c } +/// How much of the **builder side** of a request the IR was able to address, counted +/// over the emitted document. +/// +/// Every counter pairs a recovered total with what stayed unrecovered, because the two +/// must never look alike: a request that carries no argument path because its builder +/// takes positional parameters is a different fact from one whose path we failed to +/// read, and "the field is absent" says neither on its own. +#[derive(Debug, Default, Clone, Copy)] +struct IqBuilderCounts { + /// Attributes whose value comes from an argument (so: not a `const` literal and not + /// a locally generated id) — how many carry an [`arg_path`], how many do not. + /// + /// [`arg_path`]: wa_ir::WapAttrDef::arg_path + arg_path_attrs: usize, + arg_path_attrs_missing: usize, + /// Element contents that carry a runtime value (not a builder-written constant). + arg_path_contents: usize, + arg_path_contents_missing: usize, + /// Children built through a `WASmaxChildren` combinator — repeated, optional, or a + /// presence flag — each of which is fed a specific argument. + arg_path_children: usize, + arg_path_children_missing: usize, + /// Request nodes carrying element content at all. The headline for "a request's + /// payload survives the mixin boundary": a `` with no content is a group + /// rename with no name in it. + element_values: usize, + /// Children by cardinality class. `presence_flag_children` is the one a consumer can + /// model as a `bool`, and the one that was indistinguishable from an empty required + /// child before it was classified. + optional_children: usize, + presence_flag_children: usize, + /// Repeated children carrying at least one literal `REPEATED_CHILD` bound. A + /// repeated child with a `repeat_min` and no `repeat_max` is explicitly unbounded + /// (WA writes `1/0`); one with neither came from a `.map()` that states no bound. + repeat_bounds: usize, +} + +/// The `diagnostics.iq.builder` object. Built outside the manifest's `json!` literal +/// because that literal is already at the macro's recursion limit; nesting one more +/// level inside it fails to expand. +fn iq_builder_diagnostics(b: &IqBuilderCounts) -> serde_json::Value { + serde_json::json!({ + "argPathAttrs": b.arg_path_attrs, + "argPathAttrsMissing": b.arg_path_attrs_missing, + "argPathContents": b.arg_path_contents, + "argPathContentsMissing": b.arg_path_contents_missing, + "argPathChildren": b.arg_path_children, + "argPathChildrenMissing": b.arg_path_children_missing, + "elementValues": b.element_values, + "optionalChildren": b.optional_children, + "presenceFlagChildren": b.presence_flag_children, + "repeatBounds": b.repeat_bounds, + }) +} + +/// Count the builder-side coverage of an emitted IQ IR (see [`IqBuilderCounts`]). +fn iq_builder_counts(ir: &wa_ir::IqIr) -> IqBuilderCounts { + fn walk(node: &wa_ir::WapChildNode, c: &mut IqBuilderCounts) { + for a in &node.attrs { + count_attr(a, c); + } + if let Some(content) = &node.content { + c.element_values += 1; + // A `const`/`const_bytes` payload is written by the builder itself, so there + // is no argument to address and its absence is not a gap. + if content.reads_argument() { + if content.arg_path.is_some() { + c.arg_path_contents += 1; + } else { + c.arg_path_contents_missing += 1; + } + } + } + match node.presence { + wa_ir::WapChildPresence::Optional => c.optional_children += 1, + wa_ir::WapChildPresence::PresenceFlag => c.presence_flag_children += 1, + wa_ir::WapChildPresence::Required => {} + } + if node.repeats && (node.repeat_min.is_some() || node.repeat_max.is_some()) { + c.repeat_bounds += 1; + } + if node.is_combinator_fed() { + if node.arg_path.is_some() { + c.arg_path_children += 1; + } else { + c.arg_path_children_missing += 1; + } + } + for g in &node.variant_groups { + for v in &g.variants { + for a in &v.attrs { + count_attr(a, c); + } + for child in &v.children { + walk(child, c); + } + } + } + for child in &node.children { + walk(child, c); + } + } + fn count_attr(a: &wa_ir::WapAttrDef, c: &mut IqBuilderCounts) { + // An attribute whose value the BUILDER supplies reads no argument, so it can be + // missing none: a const literal, a `wap.generateId()` id, and an + // `OPTIONAL_LITERAL` whose recorded `value` is the literal and whose argument is + // a presence flag rather than the value. Counting the last as unresolved reported + // eight extraction gaps that do not exist — and contradicted the contract test, + // which already asks whether an attribute reads an argument at all. + if !a.reads_argument() { + return; + } + if a.arg_path.is_some() { + c.arg_path_attrs += 1; + } else { + c.arg_path_attrs_missing += 1; + } + } + + let mut c = IqBuilderCounts::default(); + for s in &ir.stanzas { + for child in &s.request.children { + walk(child, &mut c); + } + } + c +} + /// A loaded bundle set: the stamped version, the concatenated source the extractors /// consume, and the per-bundle identities that fingerprint the inputs (for the lock). struct Loaded { @@ -1982,6 +2121,13 @@ fn build_artifacts(wa_version: &str, source: &str) -> Result<(Vec, Cou iq_typed_error_variants: iq_diag.constraints.typed_error_variants, iq_error_texts: iq_diag.constraints.error_texts, iq_error_arms: iq_diag.constraints.error_arms, + iq_arg_path_attrs: iq_diag.builder.arg_path_attrs, + iq_arg_path_contents: iq_diag.builder.arg_path_contents, + iq_arg_path_children: iq_diag.builder.arg_path_children, + iq_element_values: iq_diag.builder.element_values, + iq_optional_children: iq_diag.builder.optional_children, + iq_presence_flag_children: iq_diag.builder.presence_flag_children, + iq_repeat_bounds: iq_diag.builder.repeat_bounds, notif_actions, notif_action_shapes, stanza_defs: stanza_count, @@ -2100,6 +2246,7 @@ fn build_artifacts(wa_version: &str, source: &str) -> Result<(Vec, Cou "errorTexts": iq_diag.constraints.error_texts, "errorArms": iq_diag.constraints.error_arms, }, + "builder": iq_builder_diagnostics(&iq_diag.builder), }, "notif": { "types": counts.notif_types, @@ -2228,6 +2375,28 @@ fn check_floor(out: &Path, counts: &Counts) -> Result> { } } } + // The builder layer. Same reasoning as the constraint counters above: each keys + // on a distinct JS construct (the destructure that names an argument, the + // `smax(tag, attrs, value)` third argument, the three `WASmaxChildren` + // combinators), so a refactor that hides one fails the update rather than + // quietly shipping an IR that can no longer address WA's own builder. + if let Some(b) = iq.get("builder") { + for (key, new) in [ + ("argPathAttrs", counts.iq_arg_path_attrs), + ("argPathContents", counts.iq_arg_path_contents), + ("argPathChildren", counts.iq_arg_path_children), + ("elementValues", counts.iq_element_values), + ("optionalChildren", counts.iq_optional_children), + ("presenceFlagChildren", counts.iq_presence_flag_children), + ("repeatBounds", counts.iq_repeat_bounds), + ] { + if let Some(prev) = b.get(key).and_then(serde_json::Value::as_u64) + && (new as u64) < prev + { + regressions.push(format!("iq.builder.{key}: {prev} → {new}")); + } + } + } } // Notification coverage below the catalog count: a drop in typed-content means a // handler's parser stopped resolving; a drop in stanzaTags means the tag-switch @@ -2600,6 +2769,7 @@ fn push_iq( drops_by_reason, cross_module, constraints: iq_constraint_counts(&ir), + builder: iq_builder_counts(&ir), targets: IqTargetCounts::of(&ir.stanzas), }; diff --git a/generated/abprops/index.json b/generated/abprops/index.json index e9f8a2c..e980d14 100644 --- a/generated/abprops/index.json +++ b/generated/abprops/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "configs": [ { diff --git a/generated/appstate/index.json b/generated/appstate/index.json index 1591817..8bf5aac 100644 --- a/generated/appstate/index.json +++ b/generated/appstate/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "collections": [ "regular", diff --git a/generated/enums/index.json b/generated/enums/index.json index 4da23a2..7777943 100644 --- a/generated/enums/index.json +++ b/generated/enums/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "enums": [ { diff --git a/generated/incoming/index.json b/generated/incoming/index.json index 0d65b2c..22fd1fc 100644 --- a/generated/incoming/index.json +++ b/generated/incoming/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "incoming": [ { diff --git a/generated/iq/index.json b/generated/iq/index.json index 2ea6016..4fb1fc3 100644 --- a/generated/iq/index.json +++ b/generated/iq/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "stanzas": [ { @@ -29,12 +29,22 @@ { "name": "hash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "propsHash" + } + ] }, { "name": "refresh_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "propsRefreshId" + } + ] } ], "children": [], @@ -677,12 +687,22 @@ { "name": "group", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "propsGroup" + } + ] }, { "name": "hash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "propsHash" + } + ] } ], "children": [], @@ -1218,11 +1238,25 @@ { "name": "dhash", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "itemArgs" + }, + { + "key": "itemDhash" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "itemArgs" + } + ] } ] }, @@ -2331,7 +2365,12 @@ { "name": "dhash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemDhash" + } + ] } ], "children": [ @@ -2341,41 +2380,103 @@ { "name": "reason", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutReason" + } + ] }, { "name": "reason_description", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutReasonDescription" + } + ] }, { "name": "entry_point", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutEntryPoint" + } + ] }, { "name": "first_message", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutFirstMessage" + } + ] }, { "name": "business_discovery_entry_point", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutBusinessDiscoveryEntryPoint" + } + ] }, { "name": "business_discovery_timestamp", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutBusinessDiscoveryTimestamp" + } + ] }, { "name": "business_discovery_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "bizOptOutArgs" + }, + { + "key": "bizOptOutBusinessDiscoveryId" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "bizOptOutArgs" + } + ] } ], "repeats": false @@ -3506,12 +3607,22 @@ { "name": "v", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "botV" + } + ] }, { "name": "bhash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "botBhash" + } + ] } ], "children": [ @@ -3521,11 +3632,27 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "botArgs", + "list": true + }, + { + "key": "botJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "argPath": [ + { + "key": "botArgs", + "list": true + } + ] } ], "repeats": false @@ -4774,12 +4901,18 @@ { "name": "pqsupport", "kind": "optional", + "value": "true", "required": false }, { "name": "context_jid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "keyContextJid" + } + ] } ], "children": [ @@ -4789,16 +4922,34 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "userArgs", + "list": true + }, + { + "key": "userJid" + } + ] }, { "name": "reason", "kind": "optional", + "value": "identity", "required": false } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 100000, + "argPath": [ + { + "key": "userArgs", + "list": true + } + ] } ], "repeats": false @@ -5999,12 +6150,18 @@ { "name": "pqsupport", "kind": "optional", + "value": "true", "required": false }, { "name": "context_jid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "keyFetchContextJid" + } + ] } ], "children": [ @@ -6014,11 +6171,21 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "userArgs", + "list": true + }, + { + "key": "userJid" + } + ] }, { "name": "reason", "kind": "optional", + "value": "identity", "required": false } ], @@ -6029,14 +6196,47 @@ { "name": "id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "userArgs", + "list": true + }, + { + "key": "deviceArgs", + "list": true + }, + { + "key": "deviceId" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 100, + "argPath": [ + { + "key": "userArgs", + "list": true + }, + { + "key": "deviceArgs", + "list": true + } + ] } ], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 100000, + "argPath": [ + { + "key": "userArgs", + "list": true + } + ] } ], "repeats": false @@ -7764,13 +7964,25 @@ "tag": "list", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasList" + } + ] }, { "tag": "pq_list", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasPqList" + } + ] }, { "tag": "op", @@ -9056,6 +9268,14 @@ "tag": "code", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "codeElementValue" + } + ] + }, "repeats": false } ], @@ -9652,11 +9872,25 @@ { "name": "scope", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "identifierArgs" + }, + { + "key": "identifierScope" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "identifierArgs" + } + ] } ] }, @@ -11983,6 +12217,9 @@ "tag": "query", "attrs": [], "children": [], + "content": { + "kind": "dynamic" + }, "repeats": false } ], @@ -12280,16 +12517,38 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "mediaArgs" + }, + { + "key": "mediaId" + } + ] }, { "name": "type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "mediaArgs" + }, + { + "key": "mediaType" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "mediaArgs" + } + ] }, { "tag": "media_list", @@ -12297,16 +12556,42 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "mediaListArgs", + "list": true + }, + { + "key": "mediaListId" + } + ] }, { "name": "type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "mediaListArgs", + "list": true + }, + { + "key": "mediaListType" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 10, + "argPath": [ + { + "key": "mediaListArgs", + "list": true + } + ] } ] }, @@ -13362,7 +13647,12 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "linkCodeCompanionRegJid" + } + ] }, { "name": "stage", @@ -13376,18 +13666,42 @@ "tag": "link_code_pairing_wrapped_key_bundle", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "linkCodePairingWrappedKeyBundleElementValue" + } + ] + }, "repeats": false }, { "tag": "companion_identity_public", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "companionIdentityPublicElementValue" + } + ] + }, "repeats": false }, { "tag": "link_code_pairing_ref", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "linkCodePairingRefElementValue" + } + ] + }, "repeats": false } ], @@ -13611,7 +13925,12 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "linkCodeCompanionRegJid" + } + ] }, { "name": "stage", @@ -13622,7 +13941,12 @@ { "name": "should_show_push_notification", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "linkCodeCompanionRegShouldShowPushNotification" + } + ] } ], "children": [ @@ -13630,12 +13954,28 @@ "tag": "link_code_pairing_wrapped_companion_ephemeral_pub", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "linkCodePairingWrappedCompanionEphemeralPubElementValue" + } + ] + }, "repeats": false }, { "tag": "companion_server_auth_key_pub", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "companionServerAuthKeyPubElementValue" + } + ] + }, "repeats": false }, { @@ -13658,9 +13998,23 @@ "kind": "bytes", "byteLength": 1, "constBytes": "00", - "valueSource": "const:linkCodePairingNonceElementValue" + "valueSource": "const:linkCodePairingNonceElementValue", + "argPath": [ + { + "key": "linkCodePairingNonceArgs" + }, + { + "key": "linkCodePairingNonceElementValue" + } + ] }, - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "linkCodePairingNonceArgs" + } + ] } ], "repeats": false @@ -14048,6 +14402,14 @@ "tag": "companion_nonce", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "companionNonceElementValue" + } + ] + }, "repeats": false } ] @@ -14128,6 +14490,14 @@ "tag": "encrypted_pairing_request", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "encryptedPairingRequestElementValue" + } + ] + }, "repeats": false } ] @@ -14213,25 +14583,66 @@ "tag": "credential_id", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "credentialIdElementValue" + } + ] + }, "repeats": false }, { "tag": "webauthn_assertion", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "webauthnAssertionElementValue" + } + ] + }, "repeats": false }, { "tag": "prologue_payload", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "prologuePayloadElementValue" + } + ] + }, "repeats": false }, { "tag": "pairing_handoff_proof", "attrs": [], "children": [], - "repeats": false + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "pairingHandoffProofArgs" + }, + { + "key": "pairingHandoffProofElementValue" + } + ] + }, + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "pairingHandoffProofArgs" + } + ] } ], "repeats": false @@ -14379,6 +14790,11 @@ "namespace": "newsletter", "iqType": "get", "target": "unknown", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "message_updates", @@ -14386,12 +14802,22 @@ { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "messageUpdatesCount" + } + ] }, { "name": "since", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageUpdatesSince" + } + ] } ], "children": [], @@ -20381,7 +20807,15 @@ { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterMessageRequestPayloadMixinArgs" + }, + { + "key": "messagesCount" + } + ] } ], "children": [], @@ -20400,12 +20834,34 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyJid" + } + ] }, { "name": "view_role", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyViewRole" + } + ] } ] }, @@ -20420,12 +20876,34 @@ { "name": "key", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyKey" + } + ] }, { "name": "view_role", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyViewRole" + } + ] } ] } @@ -20439,7 +20917,18 @@ { "name": "before", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterMessageRequestPayloadMixinArgs" + }, + { + "key": "messageDirectionsArgs" + }, + { + "key": "messagesBefore" + } + ] } ] }, @@ -20448,7 +20937,18 @@ { "name": "after", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterMessageRequestPayloadMixinArgs" + }, + { + "key": "messageDirectionsArgs" + }, + { + "key": "messagesAfter" + } + ] } ] } @@ -26503,6 +27003,11 @@ "namespace": "newsletter", "iqType": "get", "target": "unknown", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "question_responses", @@ -26510,12 +27015,22 @@ { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "questionResponsesServerId" + } + ] }, { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "questionResponsesCount" + } + ] } ], "children": [], @@ -27440,6 +27955,11 @@ "namespace": "newsletter", "iqType": "get", "target": "unknown", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "status_updates", @@ -27447,12 +27967,22 @@ { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "statusUpdatesCount" + } + ] }, { "name": "since", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "statusUpdatesSince" + } + ] } ], "children": [], @@ -29032,7 +29562,15 @@ { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterStatusRequestPayloadMixinArgs" + }, + { + "key": "statusesCount" + } + ] } ], "children": [], @@ -29051,12 +29589,34 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyJid" + } + ] }, { "name": "view_role", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyViewRole" + } + ] } ] }, @@ -29071,12 +29631,34 @@ { "name": "key", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyKey" + } + ] }, { "name": "view_role", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryNewsletterParamsMixinArgs" + }, + { + "key": "queryNewsletterParamsArgs" + }, + { + "key": "anyViewRole" + } + ] } ] } @@ -29090,7 +29672,18 @@ { "name": "before", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterStatusRequestPayloadMixinArgs" + }, + { + "key": "statusDirectionsArgs" + }, + { + "key": "statusesBefore" + } + ] } ] }, @@ -29099,7 +29692,18 @@ { "name": "after", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "newsletterStatusRequestPayloadMixinArgs" + }, + { + "key": "statusDirectionsArgs" + }, + { + "key": "statusesAfter" + } + ] } ] } @@ -30751,12 +31355,22 @@ { "name": "limit", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "myAddonsLimit" + } + ] }, { "name": "jid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "myAddonsJid" + } + ] } ], "children": [], @@ -31433,12 +32047,22 @@ { "name": "limit", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "myAddonsLimit" + } + ] }, { "name": "jid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "myAddonsJid" + } + ] } ], "children": [], @@ -32046,6 +32670,11 @@ "namespace": "newsletter", "iqType": "set", "target": "unknown", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "live_updates", @@ -32598,11 +33227,25 @@ { "name": "dhash", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "itemArgs" + }, + { + "key": "itemDhash" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "itemArgs" + } + ] } ] }, @@ -33174,42 +33817,82 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "itemJid" + } + ] }, { "name": "category", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "itemCategory" + } + ] }, { "name": "action", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "itemAction" + } + ] }, { "name": "dhash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemDhash" + } + ] }, { "name": "reason", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemReason" + } + ] }, { "name": "entry_point", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemEntryPoint" + } + ] }, { "name": "signup_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemSignupId" + } + ] }, { "name": "duration", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "itemDuration" + } + ] } ], "children": [], @@ -35519,12 +36202,28 @@ { "name": "action", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "name" + }, + { + "key": "action" + } + ] }, { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "name" + }, + { + "key": "wid" + } + ] } ], "children": [], @@ -35772,12 +36471,28 @@ "tag": "blinded_credential", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "blindedCredentialElementValue" + } + ] + }, "repeats": false }, { "tag": "project_name", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "projectNameElementValue" + } + ] + }, "repeats": false } ], @@ -36334,22 +37049,42 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListJid" + } + ] }, { "name": "source", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "spamListSource" + } + ] }, { "name": "subject", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSubject" + } + ] }, { "name": "spam_flow", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSpamFlow" + } + ] } ], "children": [ @@ -36359,22 +37094,56 @@ { "name": "from", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageArgs", + "list": true + }, + { + "key": "messageFrom" + } + ] }, { "name": "phash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageArgs", + "list": true + }, + { + "key": "messagePhash" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 210, + "argPath": [ + { + "key": "messageArgs", + "list": true + } + ] }, { "tag": "call", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 5, + "argPath": [ + { + "key": "callArgs", + "list": true + } + ] } ], "repeats": false @@ -36805,12 +37574,22 @@ { "name": "jid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "spamListJid" + } + ] }, { "name": "spam_flow", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSpamFlow" + } + ] } ], "children": [ @@ -36818,25 +37597,57 @@ "tag": "message", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 210, + "argPath": [ + { + "key": "messageArgs", + "list": true + } + ] }, { "tag": "call", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 210, + "argPath": [ + { + "key": "messageArgs", + "list": true + } + ] }, { "tag": "call", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 5, + "argPath": [ + { + "key": "callArgs", + "list": true + } + ] }, { "tag": "user_initiated_extension", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 5, + "argPath": [ + { + "key": "userInitiatedExtensionArgs", + "list": true + } + ] } ], "repeats": false @@ -37265,17 +38076,32 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListJid" + } + ] }, { "name": "subject", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSubject" + } + ] }, { "name": "spam_flow", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSpamFlow" + } + ] } ], "children": [ @@ -37285,11 +38111,28 @@ { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageArgs", + "list": true + }, + { + "key": "messageFrom" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 65, + "argPath": [ + { + "key": "messageArgs", + "list": true + } + ] } ], "repeats": false @@ -37717,12 +38560,22 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListJid" + } + ] }, { "name": "spam_flow", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSpamFlow" + } + ] } ], "children": [ @@ -37732,7 +38585,12 @@ { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageFrom" + } + ] } ], "children": [], @@ -38164,12 +39022,22 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListJid" + } + ] }, { "name": "spam_flow", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "spamListSpamFlow" + } + ] } ], "children": [ @@ -38179,7 +39047,12 @@ { "name": "from", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "statusFrom" + } + ] } ], "children": [], @@ -38532,6 +39405,14 @@ "tag": "status", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "content" + } + ] + }, "repeats": false } ] @@ -38577,16 +39458,41 @@ { "name": "id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "getDisclosureStageByIdArgs", + "list": true + }, + { + "key": "getDisclosureStageByIdId" + } + ] }, { "name": "t", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "getDisclosureStageByIdArgs", + "list": true + }, + { + "key": "getDisclosureStageByIdT" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "argPath": [ + { + "key": "getDisclosureStageByIdArgs", + "list": true + } + ] } ] }, @@ -38971,7 +39877,12 @@ { "name": "t", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "getUserDisclosuresT" + } + ] } ], "children": [], @@ -39551,12 +40462,22 @@ { "name": "id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "trackableId" + } + ] }, { "name": "result", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "trackableResult" + } + ] } ], "children": [], @@ -39896,7 +40817,12 @@ { "name": "tos_version", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "acceptPayTosVersion" + } + ] } ], "children": [], @@ -40718,17 +41644,59 @@ { "name": "appid", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppid" + } + ] }, { "name": "deviceid", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configDeviceid" + } + ] }, { "name": "fbid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configFbid" + } + ] } ] }, @@ -40737,37 +41705,135 @@ { "name": "platform", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configPlatform" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configId" + } + ] }, { "name": "appid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppid" + } + ] }, { "name": "app_mute", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppMute" + } + ] }, { "name": "pkey", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configPkey" + } + ] }, { "name": "voip_payload_type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configVoipPayloadType" + } + ] }, { "name": "num_acc", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configNumAcc" + } + ] } ], "children": [ @@ -40777,16 +41843,68 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemJid" + } + ] }, { "name": "mute", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemMute" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + } + ] } ] }, @@ -40795,117 +41913,426 @@ { "name": "platform", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configPlatform" + } + ] }, { "name": "version", "kind": "optional", + "value": "2", "required": false }, { "name": "id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configId" + } + ] }, { "name": "voip", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configVoip" + } + ] }, { "name": "preview", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configPreview" + } + ] }, { "name": "default", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configDefault" + } + ] }, { "name": "groups", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configGroups" + } + ] }, { "name": "call", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configCall" + } + ] }, { "name": "status_sound", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configStatusSound" + } + ] }, { "name": "lg", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configLg" + } + ] }, { "name": "lc", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configLc" + } + ] }, { "name": "background_location", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configBackgroundLocation" + } + ] }, { "name": "nse_ver", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configNseVer" + } + ] }, { "name": "nse_call", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configNseCall" + } + ] }, { "name": "nse_read", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configNseRead" + } + ] }, { "name": "nse_retry", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configNseRetry" + } + ] }, { "name": "reg_push", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configRegPush" + } + ] }, { "name": "pkey", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configPkey" + } + ] }, { "name": "voip_payload_type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configVoipPayloadType" + } + ] }, { "name": "settings", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configSettings" + } + ] }, { "name": "app_mute", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppMute" + } + ] }, { "name": "apple_watch_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppleWatchId" + } + ] }, { "name": "apple_watch_pkey", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAppleWatchPkey" + } + ] } ], "children": [ @@ -40915,26 +42342,114 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemJid" + } + ] }, { "name": "mute", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemMute" + } + ] }, { "name": "notify", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemNotify" + } + ] }, { "name": "call", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemCall" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "itemArgs", + "list": true + } + ] } ] }, @@ -40949,12 +42464,40 @@ { "name": "version", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configVersion" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configId" + } + ] } ] }, @@ -40969,7 +42512,21 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configId" + } + ] } ] }, @@ -40984,27 +42541,97 @@ { "name": "endpoint", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configEndpoint" + } + ] }, { "name": "auth", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configAuth" + } + ] }, { "name": "p256dh", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configP256dh" + } + ] }, { "name": "lg", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configLg" + } + ] }, { "name": "lc", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setSetConfig" + }, + { + "key": "configMixinsArgs" + }, + { + "key": "configLc" + } + ] } ] } @@ -41018,7 +42645,18 @@ { "name": "platform", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "setSetConfigOrSetClearMixinGroupArgs" + }, + { + "key": "setClear" + }, + { + "key": "clearPlatform" + } + ] } ], "children": [], @@ -41629,32 +43267,62 @@ { "name": "meta_verified", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresMetaVerified" + } + ] }, { "name": "marketing_messages", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresMarketingMessages" + } + ] }, { "name": "genai", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresGenai" + } + ] }, { "name": "genai_image", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresGenaiImage" + } + ] }, { "name": "meta_one", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresMetaOne" + } + ] }, { "name": "bb_pro", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "featuresBbPro" + } + ] } ], "children": [], @@ -42913,13 +44581,29 @@ "tag": "to", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 2000, + "argPath": [ + { + "key": "toArgs", + "list": true + } + ] }, { "tag": "use_ad_account", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 2000, + "argPath": [ + { + "key": "toArgs", + "list": true + } + ] } ], "repeats": false @@ -42928,13 +44612,25 @@ "tag": "use_ad_account", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasUseAdAccount" + } + ] }, { "tag": "skip_dedupe", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasSkipDedupe" + } + ] }, { "tag": "offer", @@ -42942,11 +44638,25 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "offerArgs" + }, + { + "key": "offerId" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "offerArgs" + } + ] }, { "tag": "pending_campaigns", @@ -42958,19 +44668,60 @@ { "name": "free_reserved_msgs", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "pendingCampaignsArgs" + }, + { + "key": "campaignArgs", + "list": true + }, + { + "key": "campaignFreeReservedMsgs" + } + ] }, { "name": "send_timestamp", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pendingCampaignsArgs" + }, + { + "key": "campaignArgs", + "list": true + }, + { + "key": "campaignSendTimestamp" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 200, + "argPath": [ + { + "key": "pendingCampaignsArgs" + }, + { + "key": "campaignArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "pendingCampaignsArgs" + } + ] } ] }, @@ -44507,24 +46258,56 @@ "tag": "latitude", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "latitude" + } + ] + }, "repeats": false }, { "tag": "longitude", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "longitude" + } + ] + }, "repeats": false }, { "tag": "description", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "description" + } + ] + }, "repeats": false }, { "tag": "email", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "email" + } + ] + }, "repeats": false }, { @@ -44561,11 +46344,26 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "categories", + "list": true + }, + { + "key": "id" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "argPath": [ + { + "key": "categories", + "list": true + } + ] } ], "repeats": false @@ -44623,40 +46421,121 @@ "tag": "service_area", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] }, { "tag": "area_description", "attrs": [], "children": [], "content": { - "kind": "dynamic" + "kind": "dynamic", + "argPath": [ + { + "key": "serviceAreas", + "list": true + }, + { + "key": "areaDescription" + } + ] }, - "repeats": true + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] }, { "tag": "area_radius_meters", "attrs": [], "children": [], - "repeats": true + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "serviceAreas", + "list": true + }, + { + "key": "radius" + } + ] + }, + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] }, { "tag": "area_center", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] }, { "tag": "latitude", "attrs": [], "children": [], - "repeats": true + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "serviceAreas", + "list": true + }, + { + "key": "latitude" + } + ] + }, + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] }, { "tag": "longitude", "attrs": [], "children": [], - "repeats": true + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "serviceAreas", + "list": true + }, + { + "key": "longitude" + } + ] + }, + "repeats": true, + "argPath": [ + { + "key": "serviceAreas", + "list": true + } + ] } ], "repeats": false @@ -44855,7 +46734,12 @@ { "name": "biz_jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "businessJid" + } + ] } ], "children": [], @@ -44907,7 +46791,12 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "catalogWid" + } + ] } ], "children": [ @@ -44915,13 +46804,34 @@ "tag": "product", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "argPath": [ + { + "key": "productIds", + "list": true + } + ] }, { "tag": "id", "attrs": [], "children": [], - "repeats": true + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "productIds", + "list": true + } + ] + }, + "repeats": true, + "argPath": [ + { + "key": "productIds", + "list": true + } + ] } ], "repeats": false @@ -45015,17 +46925,32 @@ { "name": "action", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "userFeedbackAction" + } + ] }, { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "userFeedbackJid" + } + ] }, { "name": "feedback", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "userFeedbackFeedback" + } + ] } ], "children": [], @@ -45493,22 +47418,42 @@ { "name": "promotion_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "eventPromotionId" + } + ] }, { "name": "type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "eventType" + } + ] }, { "name": "timestamp_sec", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "eventTimestampSec" + } + ] }, { "name": "logdata", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "eventLogdata" + } + ] } ], "children": [], @@ -46131,7 +48076,12 @@ { "name": "action", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "blockingAction" + } + ] } ], "children": [], @@ -46489,7 +48439,12 @@ { "name": "context", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryContext" + } + ] } ], "children": [ @@ -46499,11 +48454,28 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "groupArgs", + "list": true + }, + { + "key": "groupJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 10000, + "argPath": [ + { + "key": "groupArgs", + "list": true + } + ] } ], "repeats": false @@ -50334,6 +52306,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "query", @@ -50341,7 +52318,12 @@ { "name": "phash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "queryPhash" + } + ] } ], "children": [ @@ -50351,11 +52333,25 @@ { "name": "expiration", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "addRequestArgs" + }, + { + "key": "addRequestExpiration" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "addRequestArgs" + } + ] } ], "repeats": false @@ -55197,7 +57193,12 @@ { "name": "code", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "inviteCode" + } + ] } ], "children": [], @@ -58215,6 +60216,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [] }, "response": { @@ -61252,6 +63258,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "linked_groups_participants", @@ -61898,6 +63909,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "membership_approval_requests", @@ -62917,13 +64933,25 @@ "tag": "participants", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasParticipants" + } + ] }, { "tag": "description", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasDescription" + } + ] } ], "repeats": false @@ -66661,6 +68689,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "reports", @@ -67486,6 +69519,11 @@ "namespace": "w:g2", "iqType": "get", "target": "group_jid", + "targetArgPath": [ + { + "key": "groupWid" + } + ], "children": [ { "tag": "picture", @@ -67493,7 +69531,15 @@ { "name": "id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "option" + }, + { + "key": "id" + } + ] }, { "name": "type", @@ -67508,7 +69554,12 @@ { "name": "invite", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "code" + } + ] } ], "children": [], @@ -67651,6 +69702,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "accept", @@ -67658,17 +69714,32 @@ { "name": "code", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "acceptCode" + } + ] }, { "name": "expiration", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "acceptExpiration" + } + ] }, { "name": "admin", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "acceptAdmin" + } + ] } ], "children": [], @@ -68587,6 +70658,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "ack", @@ -69090,6 +71166,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "add", @@ -69101,21 +71182,56 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] }, { "name": "phone_number", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantPhoneNumber" + } + ] }, { "name": "username", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantUsername" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1024, + "argPath": [ + { + "key": "participantArgs", + "list": true + } + ] } ], "repeats": false @@ -71292,6 +73408,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "cancel_membership_requests", @@ -71303,11 +73424,28 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 19999, + "argPath": [ + { + "key": "participantArgs", + "list": true + } + ] } ], "repeats": false @@ -72122,21 +74260,56 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] }, { "name": "phone_number", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantPhoneNumber" + } + ] }, { "name": "username", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantUsername" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 19999, + "argPath": [ + { + "key": "participantArgs", + "list": true + } + ] }, { "tag": "description", @@ -72144,7 +74317,15 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "descriptionArgs" + }, + { + "key": "descriptionId" + } + ] } ], "children": [ @@ -72174,43 +74355,85 @@ "repeats": false } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "descriptionArgs" + } + ] }, { "tag": "locked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasLocked" + } + ] }, { "tag": "announcement", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasAnnouncement" + } + ] }, { "tag": "parent", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "parentArgs" + } + ] }, { "tag": "membership_approval_mode", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "parentArgs" + } + ] }, { "tag": "locked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "parentArgs" + } + ] }, { "tag": "no_frequently_forwarded", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNoFrequentlyForwarded" + } + ] }, { "tag": "ephemeral", @@ -72218,40 +74441,86 @@ { "name": "expiration", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "ephemeralArgs" + }, + { + "key": "ephemeralExpiration" + } + ] }, { "name": "trigger", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "ephemeralArgs" + }, + { + "key": "ephemeralTrigger" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "ephemeralArgs" + } + ] }, { "tag": "membership_approval_mode", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "membershipApprovalModeArgs" + } + ] }, { "tag": "locked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "membershipApprovalModeArgs" + } + ] }, { "tag": "breakout", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasBreakout" + } + ] }, { "tag": "created_as_lid", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasCreatedAsLid" + } + ] }, { "tag": "addressing_mode_override", @@ -72259,11 +74528,25 @@ { "name": "mode", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "addressingModeOverrideArgs" + }, + { + "key": "addressingModeOverrideMode" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "addressingModeOverrideArgs" + } + ] }, { "tag": "linked_parent", @@ -72271,35 +74554,73 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "linkedParentArgs" + }, + { + "key": "linkedParentJid" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "linkedParentArgs" + } + ] }, { "tag": "hidden_group", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasHiddenGroup" + } + ] }, { "tag": "allow_non_admin_sub_group_creation", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasAllowNonAdminSubGroupCreation" + } + ] }, { "tag": "create_general_chat", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasCreateGeneralChat" + } + ] }, { "tag": "capi", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasCapi" + } + ] } ], "repeats": false @@ -75694,6 +78015,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "sub_group_suggestion", @@ -76877,6 +79203,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "delete_parent", @@ -77347,6 +79678,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "join_linked_group", @@ -77354,12 +79690,22 @@ { "name": "type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "joinLinkedGroupType" + } + ] }, { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "joinLinkedGroupJid" + } + ] } ], "children": [], @@ -78220,6 +80566,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "links", @@ -78242,7 +80593,16 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "groupArgs", + "list": true + }, + { + "key": "groupJid" + } + ] } ], "children": [ @@ -78250,10 +80610,28 @@ "tag": "hidden_group", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "groupArgs", + "list": true + }, + { + "key": "hasHiddenGroup" + } + ] } ], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "groupArgs", + "list": true + } + ] } ], "repeats": false @@ -79612,6 +81990,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "membership_requests_action", @@ -79627,24 +82010,77 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "approveArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] }, { "name": "phone_number", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "approveArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantPhoneNumber" + } + ] }, { "name": "username", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "approveArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantUsername" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 19999, + "argPath": [ + { + "key": "approveArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "approveArgs" + } + ] }, { "tag": "reject", @@ -79656,24 +82092,77 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "rejectArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] }, { "name": "phone_number", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "rejectArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantPhoneNumber" + } + ] }, { "name": "username", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "rejectArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantUsername" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 19999, + "argPath": [ + { + "key": "rejectArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "rejectArgs" + } + ] } ], "repeats": false @@ -81195,6 +83684,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "admin", @@ -81210,14 +83704,43 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "promoteArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 1024, + "argPath": [ + { + "key": "promoteArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "promoteArgs" + } + ] }, { "tag": "demote", @@ -81229,14 +83752,43 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "demoteArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 1024, + "argPath": [ + { + "key": "demoteArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "demoteArgs" + } + ] } ], "repeats": false @@ -82150,6 +84702,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "promote", @@ -82161,14 +84718,43 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "promoteArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 1024, + "argPath": [ + { + "key": "promoteArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "promoteArgs" + } + ] }, { "tag": "demote", @@ -82180,14 +84766,43 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "demoteArgs" + }, + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 1024, + "argPath": [ + { + "key": "demoteArgs" + }, + { + "key": "participantArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "demoteArgs" + } + ] } ] }, @@ -83224,6 +85839,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "remove", @@ -83231,6 +85851,7 @@ { "name": "linked_groups", "kind": "optional", + "value": "true", "required": false } ], @@ -83241,11 +85862,28 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1024, + "argPath": [ + { + "key": "participantArgs", + "list": true + } + ] } ], "repeats": false @@ -84309,6 +86947,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "reports", @@ -84320,7 +86963,12 @@ { "name": "message_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "reportMessageId" + } + ] } ], "children": [], @@ -84877,6 +87525,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "revoke", @@ -84888,11 +87541,28 @@ { "name": "jid", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participantArgs", + "list": true + }, + { + "key": "participantJid" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "participantArgs", + "list": true + } + ] } ], "repeats": false @@ -85659,6 +88329,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "description", @@ -85666,16 +88341,27 @@ { "name": "id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "descriptionId" + } + ] }, { "name": "prev", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "descriptionPrev" + } + ] }, { "name": "delete", "kind": "optional", + "value": "true", "required": false } ], @@ -85684,7 +88370,24 @@ "tag": "body", "attrs": [], "children": [], - "repeats": false + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "bodyArgs" + }, + { + "key": "bodyElementValue" + } + ] + }, + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "bodyArgs" + } + ] } ], "repeats": false @@ -86405,24 +89108,47 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "locked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasLocked" + } + ] }, { "tag": "announcement", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasAnnouncement" + } + ] }, { "tag": "no_frequently_forwarded", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNoFrequentlyForwarded" + } + ] }, { "tag": "ephemeral", @@ -86430,88 +89156,182 @@ { "name": "expiration", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "ephemeralArgs" + }, + { + "key": "ephemeralExpiration" + } + ] }, { "name": "trigger", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "ephemeralArgs" + }, + { + "key": "ephemeralTrigger" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "ephemeralArgs" + } + ] }, { "tag": "unlocked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasUnlocked" + } + ] }, { "tag": "not_announcement", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNotAnnouncement" + } + ] }, { "tag": "frequently_forwarded_ok", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasFrequentlyForwardedOk" + } + ] }, { "tag": "not_ephemeral", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNotEphemeral" + } + ] }, { "tag": "membership_approval_mode", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "membershipApprovalModeArgs" + } + ] }, { "tag": "locked", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "membershipApprovalModeArgs" + } + ] }, { "tag": "allow_admin_reports", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasAllowAdminReports" + } + ] }, { "tag": "not_allow_admin_reports", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNotAllowAdminReports" + } + ] }, { "tag": "allow_non_admin_sub_group_creation", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasAllowNonAdminSubGroupCreation" + } + ] }, { "tag": "not_allow_non_admin_sub_group_creation", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNotAllowNonAdminSubGroupCreation" + } + ] }, { "tag": "group_history", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasGroupHistory" + } + ] }, { "tag": "no_group_history", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasNoGroupHistory" + } + ] } ] }, @@ -87628,11 +90448,24 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "subject", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "subjectElementValue" + } + ] + }, "repeats": false } ] @@ -88328,6 +91161,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "sub_group_suggestions_action", @@ -88341,10 +91179,27 @@ "tag": "sub_group_suggestion", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "approveArgs" + }, + { + "key": "subGroupSuggestionArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "approveArgs" + } + ] }, { "tag": "reject", @@ -88354,10 +91209,27 @@ "tag": "sub_group_suggestion", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "rejectArgs" + }, + { + "key": "subGroupSuggestionArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "rejectArgs" + } + ] }, { "tag": "cancel", @@ -88367,10 +91239,27 @@ "tag": "sub_group_suggestion", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "cancelArgs" + }, + { + "key": "subGroupSuggestionArgs", + "list": true + } + ] } ], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "cancelArgs" + } + ] } ], "repeats": false @@ -89811,6 +92700,11 @@ "namespace": "w:g2", "iqType": "set", "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], "children": [ { "tag": "unlink", @@ -89829,16 +92723,34 @@ { "name": "jid", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "groupArgs", + "list": true + }, + { + "key": "groupJid" + } + ] }, { "name": "remove_orphaned_members", "kind": "optional", + "value": "true", "required": false } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 1000, + "argPath": [ + { + "key": "groupArgs", + "list": true + } + ] } ], "repeats": false @@ -91054,7 +93966,12 @@ { "name": "device_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "accountDeviceId" + } + ] }, { "name": "country", @@ -91070,17 +93987,32 @@ { "name": "type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "customPaymentMethodType" + } + ] }, { "name": "update", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "customPaymentMethodUpdate" + } + ] }, { "name": "flow", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "customPaymentMethodFlow" + } + ] } ], "children": [], @@ -91556,7 +94488,12 @@ { "name": "credential_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "accountCredentialId" + } + ] }, { "name": "country", @@ -97819,32 +100756,62 @@ { "name": "type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pictureType" + } + ] }, { "name": "id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pictureId" + } + ] }, { "name": "query", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pictureQuery" + } + ] }, { "name": "invite", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pictureInvite" + } + ] }, { "name": "persona_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "picturePersonaId" + } + ] }, { "name": "common_gid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "pictureCommonGid" + } + ] } ], "children": [], @@ -98678,7 +101645,15 @@ { "name": "id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "option" + }, + { + "key": "id" + } + ] }, { "name": "type", @@ -98698,17 +101673,32 @@ { "name": "code", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "code" + } + ] }, { "name": "expiration", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "expiration" + } + ] }, { "name": "admin", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "adminWid" + } + ] } ], "children": [], @@ -98838,10 +101828,23 @@ { "name": "t", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "addT" + } + ] } ], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "addElementValue" + } + ] + }, "repeats": false } ] @@ -99221,6 +102224,9 @@ "tag": "patch", "attrs": [], "children": [], + "content": { + "kind": "dynamic" + }, "repeats": false } ], @@ -99286,18 +102292,42 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "fbid", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "fbidElementValue" + } + ] + }, "repeats": false }, { "tag": "action", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "actionElementValue" + } + ] + }, "repeats": false } ] @@ -100182,13 +103212,38 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "only_if_suspended", "attrs": [], "children": [], - "repeats": false + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "onlyIfSuspendedArgs" + }, + { + "key": "onlyIfSuspendedElementValue" + } + ] + }, + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "onlyIfSuspendedArgs" + } + ] } ] }, @@ -100573,6 +103628,14 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false } ] @@ -101138,18 +104201,42 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "fbid", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "fbidElementValue" + } + ] + }, "repeats": false }, { "tag": "id_sign", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "idSignElementValue" + } + ] + }, "repeats": false } ] @@ -101993,6 +105080,14 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { @@ -102001,22 +105096,42 @@ { "name": "id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "disclosureId" + } + ] }, { "name": "version", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "disclosureVersion" + } + ] }, { "name": "lg", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "disclosureLg" + } + ] }, { "name": "lc", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "disclosureLc" + } + ] } ], "children": [], @@ -102757,19 +105872,39 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "payload_enc_certificates", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasPayloadEncCertificates" + } + ] }, { "tag": "password_pem", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasPasswordPem" + } + ] } ] }, @@ -103400,12 +106535,28 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "fbid", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "fbidElementValue" + } + ] + }, "repeats": false } ] @@ -104269,6 +107420,14 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false } ] @@ -104742,12 +107901,28 @@ "tag": "timestamp", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "timestampElementValue" + } + ] + }, "repeats": false }, { "tag": "fbid", "attrs": [], "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "fbidElementValue" + } + ] + }, "repeats": false } ] diff --git a/generated/manifest.json b/generated/manifest.json index 3677c1c..76e6ee6 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -9,6 +9,18 @@ } }, "iq": { + "builder": { + "argPathAttrs": 218, + "argPathAttrsMissing": 55, + "argPathChildren": 108, + "argPathChildrenMissing": 12, + "argPathContents": 44, + "argPathContentsMissing": 23, + "elementValues": 67, + "optionalChildren": 33, + "presenceFlagChildren": 31, + "repeatBounds": 35 + }, "candidateModules": 207, "constraints": { "errorArms": 645, @@ -62,37 +74,37 @@ "abprops": { "file": "abprops/index.json", "schema": "schema/abprops.schema.json", - "sha256": "8f55c73c67c6ee632d81f3240861883ec0026abc524bdb32a73dcd500b031336" + "sha256": "21f6f4a28fef514bf2f4e9006839c946d9a6b9e2df376c7170bd2e8520ae9abd" }, "appstate": { "file": "appstate/index.json", "schema": "schema/appstate.schema.json", - "sha256": "86fd39f3e7c709fe6f44c6c968aa0ef73203f321bba5b95815ba0c0738fb139f" + "sha256": "655e50ecd5f3e592a9c5e5a32a98cfd40d9aa2e6a4b142f3f9203750116d6092" }, "enums": { "file": "enums/index.json", "schema": "schema/enums.schema.json", - "sha256": "bd8bc2a97ab35967978aeca3aa0b0b48e95da1a0f0aa4f062e01d2e4cb457799" + "sha256": "e0f9d1d71ab254f14a7ac2725db50e6f820a408900feb422bca481292e46c104" }, "incoming": { "file": "incoming/index.json", "schema": "schema/incoming.schema.json", - "sha256": "dc5700e2f8400d76f2b49876cbd860504d63b05f59db0ec3c9f67b858f9a1c83" + "sha256": "990d1ba619cde34bb07f929d4360618f024f388ad0c2a7ebdac6dbb516882750" }, "iq": { "file": "iq/index.json", "schema": "schema/iq.schema.json", - "sha256": "abe219f06bea1138af6b6e6970f47ace0120dfb8d87e34eed274aa4121338a9b" + "sha256": "e8e68c739a74e7551393e786eb9e63ae413209aa46330549135b7912bfa2431c" }, "mex": { "file": "mex/index.json", "schema": "schema/mex.schema.json", - "sha256": "7d5fd5ff2dde0acebbc1b780eb728f93beb5b1490c8c56896bd5ad10292e4826" + "sha256": "7d14b1daf2169e9647adb3f18d31482bb7e0271344774ed7ae0e937392d8dbee" }, "notif": { "file": "notif/index.json", "schema": "schema/notif.schema.json", - "sha256": "a9bf2d409d11eb52fc03d39c8a6d8c170f4e5920134c6cd425191d688279f6a6" + "sha256": "bd9637539c7d439746fc1866e625ab5f7cbce333350b01e1322a3494496a1018" }, "proto": { "file": "proto/WAProto.proto", @@ -101,27 +113,27 @@ "srvreq": { "file": "srvreq/index.json", "schema": "schema/srvreq.schema.json", - "sha256": "785f32b10c5a92a6f9e43e07efba4af63e4602cdd5857a5323df06b353ee35b1" + "sha256": "b7cc61c5811b028e24cd82e1ad54c1e26c5f6d1ccc8742515937e09e259bea9e" }, "stanza": { "file": "stanza/index.json", "schema": "schema/stanza.schema.json", - "sha256": "95aee43b67cf84ce7556298bc110f2af09cb7bdc85ed3351152285e6e38309ee" + "sha256": "5b0819a64a5d938759ddc58e1bd86f7594de0ba8bcacdf7d8a0013f8dc3b92d2" }, "tokens": { "file": "tokens/index.json", "schema": "schema/tokens.schema.json", - "sha256": "a7ffc66bedf994e007268ace0ce64a17c0bb04e8d28fcd4770c1d1aa25d70ca6" + "sha256": "676ab4a648f64397e2b571293dee683bb27efc3a74dc58ef7b52390782ce960f" }, "wam": { "file": "wam/index.json", "schema": "schema/wam.schema.json", - "sha256": "4bc2f0147ffd95e1d46d0c3c1c032ffcc41e210a7e1eaf7c34c7826a62778ae1" + "sha256": "6e3ecf31c667b8b71042c6a0dd7708bf4677a3c86d8f1372b1f9441cab8fa75f" }, "wasm": { "file": "wasm/index.json", "schema": "schema/wasm.schema.json", - "sha256": "dd590de1b1f785057d7c5a722f95b356865d50c9c8b687c83b8ed65e6a445df7" + "sha256": "02afd546f61f089220858f350220e94c6d056e9ac9ea7a94339d54f99b22d156" } }, "enumDefs": 403, @@ -131,7 +143,7 @@ "mexOperations": 137, "notifTypes": 27, "protoEntities": 268, - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "serverRequestDefs": 22, "stanzaDefs": 179, "tokenDoubleByte": 1024, diff --git a/generated/mex/index.json b/generated/mex/index.json index da0c776..c34bbbc 100644 --- a/generated/mex/index.json +++ b/generated/mex/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "operations": { "ACSServerProviderConfig": { diff --git a/generated/notif/index.json b/generated/notif/index.json index 36bd2c4..0ec2a4e 100644 --- a/generated/notif/index.json +++ b/generated/notif/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "dispatcherModules": [ "WAWebCommsHandleLoggedInStanza", diff --git a/generated/schema/iq.schema.json b/generated/schema/iq.schema.json index d56b78e..7713e3e 100644 --- a/generated/schema/iq.schema.json +++ b/generated/schema/iq.schema.json @@ -175,6 +175,16 @@ }, "target": { "$ref": "#/$defs/IqTarget" + }, + "targetArgPath": { + "description": "Where the addressee comes from in the builder's argument object, when the builder\nwrites a `to` it reads from an argument rather than from a constant.\n\n[`target`] says WHAT kind of addressee the request takes — a group's own JID, the\ngroup server, `s.whatsapp.net` — and for the runtime ones a consumer calling the\nvendor builder still has to know where to put it. Absent when the addressee is a\ncompile-time constant (nothing to supply), when the builder writes no `to`, and\nwhen the path is not structurally recoverable, which is counted like every other\nmissing address rather than guessed.\n\n[`target`]: IqRequestDef::target", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } } }, "required": [ @@ -758,10 +768,37 @@ "reason" ] }, + "WapArgSegment": { + "description": "One step of a [`WapArgPath`] — a key in the builder's argument object, plus\nwhether that key holds the **list** a repeated child iterates.\n\n`list` is set on one kind of segment: the array argument of a\n`WASmaxChildren.REPEATED_CHILD(template, list, min, max)` call, which invokes the\ntemplate once per element. It is *not* set for `OPTIONAL_CHILD`/`HAS_OPTIONAL_CHILD`,\nwhere the argument object is handed to the template as-is. A path may carry several,\none per repeated combinator it passes through — `userArgs[] → deviceArgs[] → deviceId`\nis read off an element of an element. The distinction is\nload-bearing rather than cosmetic: writing `participantArgs.participantJid` where the\nbuilder reads `participantArgs[0].participantJid` puts the value somewhere the vendor\nbuilder never looks, and the stanza goes out without it.\n\nIt does not say how long the list may be — that is [`WapChildNode::repeat_min`] /\n[`repeat_max`] on the node the path addresses — and it does not say the key is\npresent at runtime.\n\n[`repeat_max`]: WapChildNode::repeat_max", + "type": "object", + "properties": { + "key": { + "description": "The property name read off the argument object (`participantArgs`, `iqTo`, …).", + "type": "string" + }, + "list": { + "description": "This key holds the array a `REPEATED_CHILD` iterates; the next segment (and\neverything below it) is read off an *element*, not off the array.", + "type": "boolean" + } + }, + "required": [ + "key" + ] + }, "WapAttrDef": { "description": "A single attribute on a request stanza node.", "type": "object", "properties": { + "argPath": { + "description": "Where this attribute's value comes from in the builder's argument object — an\nabsolute [`WapArgPath`]. Absent for a [`WapAttrKind::Const`] (the builder writes\nthe literal itself, there is no argument) and whenever the path is not\nstructurally recoverable; those are counted, not guessed.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "enumRef": { "description": "The wire enum this attribute's value comes from, when the builder writes it via\na structural enum reference — `CUSTOM_STRING(o(\"Mod\").EnumName.VARIANT)` or a\n`cond === o(\"Mod\").EnumName.VARIANT ? … : DROP_ATTR` guard. Carries the enum's\nfull `(name, module, variants)` so a consumer can type the attribute as that\nenum instead of a bare string. Resolved from the enum's definition (whether a\n`$InternalEnum` or a plain object literal); absent when the value is a literal,\na variable, or any non-enum expression (never guessed from value coincidence).", "anyOf": [ @@ -783,7 +820,7 @@ "type": "boolean" }, "value": { - "description": "Present only for [`WapAttrKind::Const`].", + "description": "The fixed wire value the builder writes itself.\n\nPresent for a [`WapAttrKind::Const`] attribute, and for an\n[`Optional`](WapAttrKind::Optional) one built by\n`WASmaxAttrs.OPTIONAL_LITERAL(lit, flag)` — there the value is this literal and\nthe argument is a boolean deciding whether the attribute is written at all, the\nattribute analogue of [`WapChildPresence::PresenceFlag`]. Such an attribute\ncarries no [`arg_path`]: there is no address for a value the builder supplies, and\npointing at the boolean would tell a consumer to put the wire string there.\n\nIt does not say the attribute is always sent — only what it says when it is.\n\n[`arg_path`]: WapAttrDef::arg_path", "type": [ "string", "null" @@ -850,6 +887,16 @@ "description": "A node in a request stanza tree.", "type": "object", "properties": { + "argPath": { + "description": "Where this node's own argument object lives in the builder's arguments — the\nlist for a [`repeats`] child (its last segment carries [`WapArgSegment::list`]),\nthe argument object for an [`WapChildPresence::Optional`] one, the boolean for a\n[`WapChildPresence::PresenceFlag`] one.\n\nAbsent for a node the builder constructs inline: such a node has no argument\nobject of its own, and its attrs/content carry absolute paths regardless, so a\nconsumer never has to concatenate.\n\n[`repeats`]: WapChildNode::repeats", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "attrs": { "description": "Attributes always present on this node (independent of any variant group).", "type": "array", @@ -864,7 +911,7 @@ } }, "content": { - "description": "The leaf element content, when this node carries a value instead of child\nnodes (``, ``, `` in a prekey ``). `None` for\ncontainer nodes and attr-only nodes.", + "description": "The leaf element content, when this node carries a value instead of child\nnodes (``, ``, `` in a prekey ``). `None` for\ncontainer nodes and attr-only nodes.\n\nSays what the element carries when the builder writes it, not that it is always\nwritten: a payload contributed by an `optionalMerge` onto a node built elsewhere\nis stated here even though that merge can be skipped. There is no per-content\noptionality in this contract — [`presence`] is a property of the node.\n\n[`presence`]: WapChildNode::presence", "anyOf": [ { "$ref": "#/$defs/WapContent" @@ -874,6 +921,28 @@ } ] }, + "presence": { + "description": "Whether the builder emits this child unconditionally, only when its arguments\nare supplied, or only as a presence marker. See [`WapChildPresence`]; defaults\nto [`WapChildPresence::Required`], which is also what a node whose call site was\nnot one of the `WASmaxChildren` combinators gets.", + "$ref": "#/$defs/WapChildPresence" + }, + "repeatMax": { + "description": "Upper bound on the number of repetitions — the 4th argument of `REPEATED_CHILD`\n(`add/participant` caps at 1024, `query/group` at 10000). Same presence rules as\n[`repeat_min`].\n\n[`repeat_min`]: WapChildNode::repeat_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "repeatMin": { + "description": "Lower bound on the number of repetitions, from the 3rd argument of\n`REPEATED_CHILD(template, list, min, max)`. Present only when [`repeats`] is set\nand the bound is a literal; a computed or non-finite bound is omitted and\ncounted, never defaulted to 0.\n\nIt is the bound *this builder* enforces before sending. The server enforces its\nown, which may be stricter.\n\n[`repeats`]: WapChildNode::repeats", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "repeats": { "description": "Whether this child can appear multiple times (maps to `Vec<_>` in codegen).", "type": "boolean" @@ -896,10 +965,40 @@ "repeats" ] }, + "WapChildPresence": { + "description": "Whether a request child node must be built, may be omitted, or exists only to\nsignal a flag — the three states `WASmaxChildren` distinguishes and the wire does\nnot.", + "oneOf": [ + { + "description": "Built unconditionally — the builder calls the child template directly, or\n`REPEATED_CHILD` iterates a list that is itself a required argument. For a\nrepeated child, \"required\" is about the *list argument*: a `repeat_min` of 0\nstill allows the list to be empty.", + "type": "string", + "const": "required" + }, + { + "description": "`WASmaxChildren.OPTIONAL_CHILD(template, args)` — emitted only when the\nargument object is supplied. The child carries real attrs/content when present.", + "type": "string", + "const": "optional" + }, + { + "description": "`WASmaxChildren.HAS_OPTIONAL_CHILD(template, flag)` — a **presence marker**:\nthe template takes no arguments, so the element is empty and its entire meaning\nis being there (`` on a group create). A consumer can model it as a\n`bool` argument, which it cannot do for [`Optional`] and must not do for an\nempty [`Required`] child.\n\nIt does not guarantee the element has no attributes on the wire — only that\nthis builder writes none.\n\n[`Optional`]: WapChildPresence::Optional", + "type": "string", + "const": "presence_flag" + } + ] + }, "WapContent": { "description": "The element content of a leaf request node — what sits between `` and\n`` when the node carries a value rather than child nodes.", "type": "object", "properties": { + "argPath": { + "description": "Where the content value comes from in the builder's argument object — an\nabsolute [`WapArgPath`] (``'s text is `subjectElementValue`). Absent for a\n[`WapContentKind::Const`] payload, which the builder writes itself, and whenever\nthe path is not structurally recoverable.\n\nA [`const_bytes`] payload keeps its path: it is *pinned* — every call site passes\nthe same constant — not written by the builder, so the argument is still read and\na consumer still has to supply it. The constant says what to put here.\n\n[`const_bytes`]: WapContent::const_bytes", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "byteLength": { "description": "Fixed byte length. Either written directly in the request builder\n(`BIG_ENDIAN_CONTENT(x, 3)` → 3, [`byte_length_source`] absent) or\ncross-referenced from the symmetric parser that reads the same wire field\n(`child(\"signature\").contentBytes(64)` → 64, [`byte_length_source`] set).\n\n[`byte_length_source`]: WapContent::byte_length_source", "type": [ diff --git a/generated/schema/stanza.schema.json b/generated/schema/stanza.schema.json index a5450f2..f070265 100644 --- a/generated/schema/stanza.schema.json +++ b/generated/schema/stanza.schema.json @@ -727,10 +727,37 @@ } ] }, + "WapArgSegment": { + "description": "One step of a [`WapArgPath`] — a key in the builder's argument object, plus\nwhether that key holds the **list** a repeated child iterates.\n\n`list` is set on one kind of segment: the array argument of a\n`WASmaxChildren.REPEATED_CHILD(template, list, min, max)` call, which invokes the\ntemplate once per element. It is *not* set for `OPTIONAL_CHILD`/`HAS_OPTIONAL_CHILD`,\nwhere the argument object is handed to the template as-is. A path may carry several,\none per repeated combinator it passes through — `userArgs[] → deviceArgs[] → deviceId`\nis read off an element of an element. The distinction is\nload-bearing rather than cosmetic: writing `participantArgs.participantJid` where the\nbuilder reads `participantArgs[0].participantJid` puts the value somewhere the vendor\nbuilder never looks, and the stanza goes out without it.\n\nIt does not say how long the list may be — that is [`WapChildNode::repeat_min`] /\n[`repeat_max`] on the node the path addresses — and it does not say the key is\npresent at runtime.\n\n[`repeat_max`]: WapChildNode::repeat_max", + "type": "object", + "properties": { + "key": { + "description": "The property name read off the argument object (`participantArgs`, `iqTo`, …).", + "type": "string" + }, + "list": { + "description": "This key holds the array a `REPEATED_CHILD` iterates; the next segment (and\neverything below it) is read off an *element*, not off the array.", + "type": "boolean" + } + }, + "required": [ + "key" + ] + }, "WapAttrDef": { "description": "A single attribute on a request stanza node.", "type": "object", "properties": { + "argPath": { + "description": "Where this attribute's value comes from in the builder's argument object — an\nabsolute [`WapArgPath`]. Absent for a [`WapAttrKind::Const`] (the builder writes\nthe literal itself, there is no argument) and whenever the path is not\nstructurally recoverable; those are counted, not guessed.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "enumRef": { "description": "The wire enum this attribute's value comes from, when the builder writes it via\na structural enum reference — `CUSTOM_STRING(o(\"Mod\").EnumName.VARIANT)` or a\n`cond === o(\"Mod\").EnumName.VARIANT ? … : DROP_ATTR` guard. Carries the enum's\nfull `(name, module, variants)` so a consumer can type the attribute as that\nenum instead of a bare string. Resolved from the enum's definition (whether a\n`$InternalEnum` or a plain object literal); absent when the value is a literal,\na variable, or any non-enum expression (never guessed from value coincidence).", "anyOf": [ @@ -752,7 +779,7 @@ "type": "boolean" }, "value": { - "description": "Present only for [`WapAttrKind::Const`].", + "description": "The fixed wire value the builder writes itself.\n\nPresent for a [`WapAttrKind::Const`] attribute, and for an\n[`Optional`](WapAttrKind::Optional) one built by\n`WASmaxAttrs.OPTIONAL_LITERAL(lit, flag)` — there the value is this literal and\nthe argument is a boolean deciding whether the attribute is written at all, the\nattribute analogue of [`WapChildPresence::PresenceFlag`]. Such an attribute\ncarries no [`arg_path`]: there is no address for a value the builder supplies, and\npointing at the boolean would tell a consumer to put the wire string there.\n\nIt does not say the attribute is always sent — only what it says when it is.\n\n[`arg_path`]: WapAttrDef::arg_path", "type": [ "string", "null" @@ -819,6 +846,16 @@ "description": "A node in a request stanza tree.", "type": "object", "properties": { + "argPath": { + "description": "Where this node's own argument object lives in the builder's arguments — the\nlist for a [`repeats`] child (its last segment carries [`WapArgSegment::list`]),\nthe argument object for an [`WapChildPresence::Optional`] one, the boolean for a\n[`WapChildPresence::PresenceFlag`] one.\n\nAbsent for a node the builder constructs inline: such a node has no argument\nobject of its own, and its attrs/content carry absolute paths regardless, so a\nconsumer never has to concatenate.\n\n[`repeats`]: WapChildNode::repeats", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "attrs": { "description": "Attributes always present on this node (independent of any variant group).", "type": "array", @@ -833,7 +870,7 @@ } }, "content": { - "description": "The leaf element content, when this node carries a value instead of child\nnodes (``, ``, `` in a prekey ``). `None` for\ncontainer nodes and attr-only nodes.", + "description": "The leaf element content, when this node carries a value instead of child\nnodes (``, ``, `` in a prekey ``). `None` for\ncontainer nodes and attr-only nodes.\n\nSays what the element carries when the builder writes it, not that it is always\nwritten: a payload contributed by an `optionalMerge` onto a node built elsewhere\nis stated here even though that merge can be skipped. There is no per-content\noptionality in this contract — [`presence`] is a property of the node.\n\n[`presence`]: WapChildNode::presence", "anyOf": [ { "$ref": "#/$defs/WapContent" @@ -843,6 +880,28 @@ } ] }, + "presence": { + "description": "Whether the builder emits this child unconditionally, only when its arguments\nare supplied, or only as a presence marker. See [`WapChildPresence`]; defaults\nto [`WapChildPresence::Required`], which is also what a node whose call site was\nnot one of the `WASmaxChildren` combinators gets.", + "$ref": "#/$defs/WapChildPresence" + }, + "repeatMax": { + "description": "Upper bound on the number of repetitions — the 4th argument of `REPEATED_CHILD`\n(`add/participant` caps at 1024, `query/group` at 10000). Same presence rules as\n[`repeat_min`].\n\n[`repeat_min`]: WapChildNode::repeat_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "repeatMin": { + "description": "Lower bound on the number of repetitions, from the 3rd argument of\n`REPEATED_CHILD(template, list, min, max)`. Present only when [`repeats`] is set\nand the bound is a literal; a computed or non-finite bound is omitted and\ncounted, never defaulted to 0.\n\nIt is the bound *this builder* enforces before sending. The server enforces its\nown, which may be stricter.\n\n[`repeats`]: WapChildNode::repeats", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "repeats": { "description": "Whether this child can appear multiple times (maps to `Vec<_>` in codegen).", "type": "boolean" @@ -865,10 +924,40 @@ "repeats" ] }, + "WapChildPresence": { + "description": "Whether a request child node must be built, may be omitted, or exists only to\nsignal a flag — the three states `WASmaxChildren` distinguishes and the wire does\nnot.", + "oneOf": [ + { + "description": "Built unconditionally — the builder calls the child template directly, or\n`REPEATED_CHILD` iterates a list that is itself a required argument. For a\nrepeated child, \"required\" is about the *list argument*: a `repeat_min` of 0\nstill allows the list to be empty.", + "type": "string", + "const": "required" + }, + { + "description": "`WASmaxChildren.OPTIONAL_CHILD(template, args)` — emitted only when the\nargument object is supplied. The child carries real attrs/content when present.", + "type": "string", + "const": "optional" + }, + { + "description": "`WASmaxChildren.HAS_OPTIONAL_CHILD(template, flag)` — a **presence marker**:\nthe template takes no arguments, so the element is empty and its entire meaning\nis being there (`` on a group create). A consumer can model it as a\n`bool` argument, which it cannot do for [`Optional`] and must not do for an\nempty [`Required`] child.\n\nIt does not guarantee the element has no attributes on the wire — only that\nthis builder writes none.\n\n[`Optional`]: WapChildPresence::Optional", + "type": "string", + "const": "presence_flag" + } + ] + }, "WapContent": { "description": "The element content of a leaf request node — what sits between `` and\n`` when the node carries a value rather than child nodes.", "type": "object", "properties": { + "argPath": { + "description": "Where the content value comes from in the builder's argument object — an\nabsolute [`WapArgPath`] (``'s text is `subjectElementValue`). Absent for a\n[`WapContentKind::Const`] payload, which the builder writes itself, and whenever\nthe path is not structurally recoverable.\n\nA [`const_bytes`] payload keeps its path: it is *pinned* — every call site passes\nthe same constant — not written by the builder, so the argument is still read and\na consumer still has to supply it. The constant says what to put here.\n\n[`const_bytes`]: WapContent::const_bytes", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/WapArgSegment" + } + }, "byteLength": { "description": "Fixed byte length. Either written directly in the request builder\n(`BIG_ENDIAN_CONTENT(x, 3)` → 3, [`byte_length_source`] absent) or\ncross-referenced from the symmetric parser that reads the same wire field\n(`child(\"signature\").contentBytes(64)` → 64, [`byte_length_source`] set).\n\n[`byte_length_source`]: WapContent::byte_length_source", "type": [ diff --git a/generated/srvreq/index.json b/generated/srvreq/index.json index 09caf09..c83a36c 100644 --- a/generated/srvreq/index.json +++ b/generated/srvreq/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "requests": [ { diff --git a/generated/stanza/index.json b/generated/stanza/index.json index ab89963..7ed9320 100644 --- a/generated/stanza/index.json +++ b/generated/stanza/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "stanzas": [ { @@ -103,7 +103,12 @@ { "name": "contenttype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaContenttype" + } + ] } ], "children": [], @@ -182,7 +187,12 @@ { "name": "contenttype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaContenttype" + } + ] } ], "children": [], @@ -224,7 +234,12 @@ { "name": "contenttype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaContenttype" + } + ] } ], "children": [], @@ -298,7 +313,13 @@ "tag": "ctwa_attribution", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "ctwaAttributionArgs" + } + ] } ], "response": null, @@ -323,7 +344,12 @@ { "name": "contenttype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaContenttype" + } + ] } ], "children": [], @@ -352,7 +378,12 @@ { "name": "message_association_type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaMessageAssociationType" + } + ] } ], "children": [], @@ -381,7 +412,12 @@ { "name": "premium_type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaPremiumType" + } + ] } ], "children": [], @@ -410,7 +446,12 @@ { "name": "origin", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaOrigin" + } + ] } ], "children": [], @@ -465,12 +506,22 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageId" + } + ] }, { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "messageServerId" + } + ] } ], "children": [], @@ -492,7 +543,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageId" + } + ] } ], "children": [], @@ -518,7 +574,12 @@ { "name": "mediatype", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "plaintextMediatype" + } + ] } ], "children": [], @@ -543,7 +604,12 @@ { "name": "media_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageMediaId" + } + ] } ], "children": [], @@ -618,7 +684,27 @@ "tag": "vote", "attrs": [], "children": [], - "repeats": true + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "voteArgs", + "list": true + }, + { + "key": "voteElementValue" + } + ] + }, + "repeats": true, + "repeatMin": 0, + "repeatMax": 1000, + "argPath": [ + { + "key": "voteArgs", + "list": true + } + ] } ], "repeats": false @@ -692,7 +778,12 @@ { "name": "code", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "reactionCode" + } + ] } ], "children": [], @@ -740,7 +831,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageTo" + } + ] } ], "children": [], @@ -811,12 +907,22 @@ { "name": "parent_server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "metaParentServerId" + } + ] }, { "name": "response_server_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaResponseServerId" + } + ] } ], "children": [], @@ -964,7 +1070,12 @@ { "name": "content_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "plaintextContentId" + } + ] } ], "children": [], @@ -1128,7 +1239,12 @@ { "name": "contenttype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaContenttype" + } + ] } ], "children": [], @@ -1179,7 +1295,15 @@ "tag": "extension_screen", "attrs": [], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 1, + "repeatMax": 100, + "argPath": [ + { + "key": "extensionScreenArgs", + "list": true + } + ] } ], "response": null, @@ -1202,12 +1326,22 @@ { "name": "from", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageFrom" + } + ] }, { "name": "phash", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messagePhash" + } + ] } ], "children": [], @@ -1232,7 +1366,12 @@ { "name": "reported_link", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "iabReportedLink" + } + ] } ], "children": [], @@ -1283,7 +1422,13 @@ "tag": "raw", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "rawArgs" + } + ] } ], "response": null, @@ -1306,27 +1451,52 @@ { "name": "t", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "messageT" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageId" + } + ] }, { "name": "selected", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageSelected" + } + ] }, { "name": "unsent", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageUnsent" + } + ] }, { "name": "deleted_reason", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageDeletedReason" + } + ] } ], "children": [ @@ -1336,16 +1506,49 @@ { "name": "mediatype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "rawArgs" + }, + { + "key": "rawMediatype" + } + ] }, { "name": "local_message_type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "rawArgs" + }, + { + "key": "rawLocalMessageType" + } + ] } ], "children": [], - "repeats": false + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "rawArgs" + }, + { + "key": "rawElementValue" + } + ] + }, + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "rawArgs" + } + ] } ], "response": null, @@ -1366,22 +1569,42 @@ { "name": "participant", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageParticipant" + } + ] }, { "name": "participant_type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageParticipantType" + } + ] }, { "name": "member_tag", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageMemberTag" + } + ] }, { "name": "member_tag_ts_s", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "messageMemberTagTsS" + } + ] } ], "children": [], @@ -1433,7 +1656,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageTo" + } + ] } ], "children": [], @@ -1455,7 +1683,12 @@ { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageFrom" + } + ] } ], "children": [], @@ -1481,7 +1714,12 @@ { "name": "tid", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "hsmTid" + } + ] } ], "children": [], @@ -1506,12 +1744,22 @@ { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "messageServerId" + } + ] }, { "name": "response_server_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageResponseServerId" + } + ] } ], "children": [], @@ -1580,7 +1828,12 @@ { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageFrom" + } + ] } ], "children": [], @@ -1647,7 +1900,12 @@ { "name": "reported_push_name", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageReportedPushName" + } + ] } ], "children": [], @@ -1696,7 +1954,12 @@ { "name": "biz_source", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaBizSource" + } + ] } ], "children": [], @@ -1721,7 +1984,12 @@ { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "messageFrom" + } + ] } ], "children": [], @@ -1745,7 +2013,13 @@ "tag": "raw", "attrs": [], "children": [], - "repeats": false + "repeats": false, + "presence": "presence_flag", + "argPath": [ + { + "key": "hasRaw" + } + ] } ], "response": null, @@ -1766,22 +2040,42 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageId" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "recipientJid" + } + ] }, { "name": "phash", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "phash" + } + ] }, { "name": "type", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "messageType" + } + ] } ], "children": [ @@ -1891,7 +2185,21 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "sendMsgRecord" + }, + { + "key": "data" + }, + { + "key": "id" + }, + { + "key": "id" + } + ] }, { "name": "to", @@ -1901,7 +2209,12 @@ { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] }, { "name": "edit", @@ -1963,7 +2276,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] } ], "children": [ @@ -1973,12 +2291,22 @@ { "name": "v", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "version" + } + ] } ], "children": [], "content": { - "kind": "dynamic" + "kind": "dynamic", + "argPath": [ + { + "key": "reportingToken" + } + ] }, "repeats": false } @@ -2000,7 +2328,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] } ], "children": [], @@ -2021,22 +2354,51 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + }, + { + "key": "id" + }, + { + "key": "id" + } + ] }, { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "participant", "kind": "device_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] }, { "name": "edit", @@ -2079,12 +2441,25 @@ { "name": "count", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "deviceMsgType" + }, + { + "key": "retryCount" + } + ] }, { "name": "mediatype", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] } ], "children": [], @@ -2114,17 +2489,41 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + }, + { + "key": "id" + }, + { + "key": "id" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] }, { "name": "edit", @@ -2390,7 +2789,21 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + }, + { + "key": "id" + }, + { + "key": "id" + } + ] }, { "name": "to", @@ -2400,27 +2813,64 @@ { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "params" + }, + { + "key": "participant" + } + ] }, { "name": "recipient", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "params" + }, + { + "key": "recipient" + } + ] }, { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] }, { "name": "peer_recipient_pn", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "params" + }, + { + "key": "recipient" + } + ] }, { "name": "peer_recipient_lid", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "params" + }, + { + "key": "peerRecipientLid" + } + ] }, { "name": "peer_recipient_username", @@ -2440,7 +2890,18 @@ { "name": "push_priority", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "params" + }, + { + "key": "option" + }, + { + "key": "pushPriority" + } + ] }, { "name": "privacy_sensitive", @@ -2485,7 +2946,12 @@ { "name": "mediatype", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] } ], "children": [], @@ -2591,12 +3057,37 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + }, + { + "key": "id" + }, + { + "key": "id" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + }, + { + "key": "to" + } + ] }, { "name": "type", @@ -2711,7 +3202,12 @@ { "name": "native_flow_name", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "msgProtobuf" + } + ] } ], "children": [], @@ -2790,7 +3286,15 @@ { "name": "st", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "scheduledMsgMetadata" + }, + { + "key": "scheduledTimestampS" + } + ] } ], "children": [ @@ -2800,12 +3304,28 @@ { "name": "rkid", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "scheduledMsgMetadata" + }, + { + "key": "revealKeyId" + } + ] } ], "children": [], "content": { - "kind": "dynamic" + "kind": "dynamic", + "argPath": [ + { + "key": "scheduledMsgMetadata" + }, + { + "key": "revealKey" + } + ] }, "repeats": false }, @@ -2815,7 +3335,15 @@ { "name": "rkid", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "scheduledMsgMetadata" + }, + { + "key": "revealKeyId" + } + ] } ], "children": [], @@ -2836,7 +3364,15 @@ { "name": "type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "msgRecord" + }, + { + "key": "data" + } + ] }, { "name": "local_automated_type", @@ -2880,7 +3416,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "receiptTo" + } + ] } ], "children": [], @@ -2903,7 +3444,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "receiptId" + } + ] } ], "children": [ @@ -2917,11 +3463,28 @@ { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "itemArgs", + "list": true + }, + { + "key": "itemServerId" + } + ] } ], "children": [], - "repeats": true + "repeats": true, + "repeatMin": 0, + "repeatMax": 255, + "argPath": [ + { + "key": "itemArgs", + "list": true + } + ] } ], "repeats": false @@ -2993,12 +3556,22 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] } ], "children": [ @@ -3026,22 +3599,42 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "externalId" + } + ] }, { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "recipient", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "recipient" + } + ] }, { "name": "type", @@ -3284,22 +3877,42 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "externalId" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "participant", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "recipient", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "recipient" + } + ] }, { "name": "type", @@ -3326,22 +3939,42 @@ { "name": "count", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "retryCount" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "externalId" + } + ] }, { "name": "t", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "rawTs" + } + ] }, { "name": "error", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "retryReason" + } + ] } ], "children": [], @@ -3397,7 +4030,15 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "id" + }, + { + "key": "id" + } + ] } ], "children": [ @@ -4101,12 +4742,22 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] }, { "name": "class", @@ -4307,7 +4958,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "class", @@ -4526,12 +5182,22 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] }, { "name": "class", @@ -4565,12 +5231,22 @@ { "name": "to", "kind": "group_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "chatId" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "externalId" + } + ] }, { "name": "class", @@ -4587,7 +5263,12 @@ { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "author" + } + ] } ], "children": [], @@ -4806,27 +5487,52 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "externalId" + } + ] }, { "name": "class", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaClass" + } + ] }, { "name": "to", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "from" + } + ] }, { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "type" + } + ] } ], "children": [], @@ -5102,7 +5808,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "class", @@ -5119,7 +5830,12 @@ { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] } ], "children": [], @@ -5140,7 +5856,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "class", @@ -5332,32 +6053,62 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "ackId" + } + ] }, { "name": "class", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "ackClass" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "type" + } + ] }, { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "error", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "error" + } + ] } ], "children": [], @@ -5394,12 +6145,22 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] } ], "children": [], @@ -5420,12 +6181,22 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "from" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "stanzaId" + } + ] }, { "name": "class", @@ -5436,12 +6207,22 @@ { "name": "type", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "ackString" + } + ] }, { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participant" + } + ] } ], "children": [], @@ -5535,17 +6316,32 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "to" + } + ] }, { "name": "recipient", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "recipient" + } + ] }, { "name": "participant", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "participant" + } + ] }, { "name": "class", @@ -5598,17 +6394,32 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "callTo" + } + ] }, { "name": "from", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "callFrom" + } + ] }, { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "callId" + } + ] } ], "children": [ @@ -5618,42 +6429,82 @@ { "name": "duration", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "callInfoDuration" + } + ] }, { "name": "terminate_reason", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoTerminateReason" + } + ] }, { "name": "reason", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoReason" + } + ] }, { "name": "terminator", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoTerminator" + } + ] }, { "name": "start_time", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoStartTime" + } + ] }, { "name": "adder", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoAdder" + } + ] }, { "name": "creator", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoCreator" + } + ] }, { "name": "mediatype", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "callInfoMediatype" + } + ] } ], "children": [], @@ -5684,7 +6535,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "callTo" + } + ] } ], "children": [ @@ -5694,16 +6550,27 @@ { "name": "media", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "linkCreateMedia" + } + ] }, { "name": "link_creator_username", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "linkCreateLinkCreatorUsername" + } + ] }, { "name": "waiting_room_enabled", "kind": "optional", + "value": "1", "required": false } ], @@ -5714,11 +6581,25 @@ { "name": "start_time", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "eventArgs" + }, + { + "key": "eventStartTime" + } + ] } ], "children": [], - "repeats": false + "repeats": false, + "presence": "optional", + "argPath": [ + { + "key": "eventArgs" + } + ] } ], "repeats": false @@ -5756,17 +6637,32 @@ { "name": "token", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "linkQueryToken" + } + ] }, { "name": "media", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "linkQueryMedia" + } + ] }, { "name": "action", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "linkQueryAction" + } + ] } ], "children": [], @@ -5805,12 +6701,22 @@ { "name": "enabled", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "waitingRoomToggleEnabled" + } + ] }, { "name": "media", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "waitingRoomToggleMedia" + } + ] } ], "children": [], @@ -5834,7 +6740,12 @@ { "name": "to", "kind": "dynamic", - "required": true + "required": true, + "argPath": [ + { + "key": "peerJid" + } + ] }, { "name": "id", @@ -5883,7 +6794,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "chatstateTo" + } + ] } ], "children": [], @@ -5908,6 +6824,7 @@ { "name": "media", "kind": "optional", + "value": "audio", "required": false } ], @@ -5956,12 +6873,22 @@ { "name": "type", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "presenceType" + } + ] }, { "name": "name", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "presenceName" + } + ] } ], "children": [], @@ -5988,17 +6915,32 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "presenceTo" + } + ] }, { "name": "name", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "presenceName" + } + ] }, { "name": "context", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "presenceContext" + } + ] } ], "children": [], @@ -6019,12 +6961,22 @@ { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "statusServerId" + } + ] }, { "name": "t", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "statusT" + } + ] } ], "children": [], @@ -6096,7 +7048,12 @@ { "name": "mediatype", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "plaintextMediatype" + } + ] } ], "children": [], @@ -6144,7 +7101,12 @@ { "name": "from", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "statusFrom" + } + ] } ], "children": [], @@ -6165,12 +7127,22 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "statusId" + } + ] }, { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "statusServerId" + } + ] } ], "children": [], @@ -6192,7 +7164,12 @@ { "name": "id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "statusId" + } + ] } ], "children": [], @@ -6214,7 +7191,12 @@ { "name": "to", "kind": "user_jid", - "required": true + "required": true, + "argPath": [ + { + "key": "statusTo" + } + ] } ], "children": [], @@ -6397,12 +7379,22 @@ { "name": "parent_server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "metaParentServerId" + } + ] }, { "name": "response_server_id", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "metaResponseServerId" + } + ] } ], "children": [], @@ -6437,7 +7429,12 @@ { "name": "response_server_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "metaResponseServerId" + } + ] } ], "children": [], @@ -6466,7 +7463,12 @@ { "name": "mediatype", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "plaintextMediatype" + } + ] } ], "children": [], @@ -6491,7 +7493,12 @@ { "name": "media_id", "kind": "optional", - "required": false + "required": false, + "argPath": [ + { + "key": "statusMediaId" + } + ] } ], "children": [], @@ -6513,7 +7520,12 @@ { "name": "server_id", "kind": "integer", - "required": true + "required": true, + "argPath": [ + { + "key": "statusServerId" + } + ] } ], "children": [ @@ -6546,7 +7558,12 @@ { "name": "code", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "reactionCode" + } + ] } ], "children": [], @@ -6833,17 +7850,32 @@ { "name": "name", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "name" + } + ] }, { "name": "value", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "value" + } + ] }, { "name": "dhash", "kind": "string", - "required": true + "required": true, + "argPath": [ + { + "key": "dhash" + } + ] } ], "children": [], diff --git a/generated/tokens/index.json b/generated/tokens/index.json index 88d6e10..96f8713 100644 --- a/generated/tokens/index.json +++ b/generated/tokens/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "dictVersion": 3, "singleByte": [ diff --git a/generated/wam/index.json b/generated/wam/index.json index bd396e4..95d3600 100644 --- a/generated/wam/index.json +++ b/generated/wam/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "events": [ { diff --git a/generated/wasm/index.json b/generated/wasm/index.json index 7d72c3f..c4eff99 100644 --- a/generated/wasm/index.json +++ b/generated/wasm/index.json @@ -1,5 +1,5 @@ { - "schemaVersion": "3.0.0", + "schemaVersion": "4.0.0", "waVersion": "2.3000.1044659339", "binaries": [ { diff --git a/scripts/lint-ir.py b/scripts/lint-ir.py index ac1303d..3cc5510 100755 --- a/scripts/lint-ir.py +++ b/scripts/lint-ir.py @@ -53,6 +53,32 @@ # once it is the shape of a claim withdrawn. "iq request with a unknown addressee": 5, "iq request with a unset addressee": 0, + # Builder-side values whose address the scan could not recover — an attribute or an + # element content that reads an argument, or a combinator-fed child, with no + # `argPath`. Counted rather than floor-guarded for the same reason as the addressees + # above, and it is the direction `diagnostics.iq.builder` cannot see: that block + # guards the RECOVERED totals against falling, so a bundle update that leaves more + # values unaddressed while unrelated ones start resolving holds those totals steady + # and says nothing. Almost all of these are the legacy `WAWeb*Job` builders, which + # take positional parameters and have no argument object to address at all; they fall + # only if those builders stop existing or start being read. + "iq builder attribute with no argument path": 55, + "iq builder content with no argument path": 23, + "iq builder child with no argument path": 12, + # A request whose addressee is supplied at runtime — a group's own JID, a newsletter's + # — and whose argument key the scan could not recover. Two, for different reasons. + # `WAWebGroupInviteJob` is a legacy positional builder with no argument object to + # address at all. `WASmaxOutGroupsGetGroupProfilePicturesRequest` folds in a runtime + # router whose arms address a group's own JID and the group server: the union reports + # `unknown` because the two disagree, and a disagreement about WHICH addressee is one + # about its address too — publishing the group arm's key beside `unknown` would + # advertise one branch's argument as the request's answer. This one is a claim + # withdrawn rather than a loss, the same correction #44 made to its `target`. + # + # It falls if the legacy family goes or the router learns to name both arms; it RISES + # if a smax request starts hiding its `to`, which is the case worth catching, since + # `target` alone tells a consumer that a value is required without telling it where. + "iq request with a runtime addressee and no argument path": 2, } # The enums no extraction path could resolve, by IDENTITY rather than by total. @@ -822,6 +848,56 @@ def count_unresolved_targets(data, domain, counts): counts[f"iq request with a {st['target']} addressee"] += 1 +def count_builder_gaps(data, domain, counts): + """Values a builder reads from its argument object whose address is not recovered. + + The same three questions `iq_builder_counts` asks in the emitter, asked of the emitted + document: an attribute reads an argument unless the builder supplies the value itself + (a const, a generated id, or a recorded fixed `value`), a content reads one unless it + is a `const` literal, and a child has an argument of its own when a combinator fed it + (it repeats, or its presence is not the default). + """ + if domain != "iq": + return + + def walk_node(n): + for a in n.get("attrs") or []: + reads = a.get("kind") not in ("const", "generated_id") and "value" not in a + if reads and "argPath" not in a: + counts["iq builder attribute with no argument path"] += 1 + c = n.get("content") + if c and c.get("kind") != "const" and "argPath" not in c: + counts["iq builder content with no argument path"] += 1 + if (n.get("repeats") or n.get("presence", "required") != "required") and ( + "argPath" not in n + ): + counts["iq builder child with no argument path"] += 1 + for g in n.get("variantGroups") or []: + for v in g.get("variants") or []: + for a in v.get("attrs") or []: + reads = a.get("kind") not in ("const", "generated_id") and "value" not in a + if reads and "argPath" not in a: + counts["iq builder attribute with no argument path"] += 1 + for ch in v.get("children") or []: + walk_node(ch) + for ch in n.get("children") or []: + walk_node(ch) + + for st in data.get("stanzas") or []: + for ch in ((st.get("request") or {}).get("children")) or []: + walk_node(ch) + + +def count_unaddressed_targets(data, domain, counts): + """Runtime addressees with no argument path — see the baseline entry.""" + if domain != "iq": + return + for st in data.get("stanzas") or []: + r = st.get("request") or {} + if r.get("target") in ("group_jid", "unknown") and "targetArgPath" not in r: + counts["iq request with a runtime addressee and no argument path"] += 1 + + def check_event_codes(data, domain, errors): """A WAM event's `code` is its wire identifier, so two events cannot share one. @@ -1287,6 +1363,168 @@ def check_variant_groups(node, path, errors): ) +def check_arg_path(node, path, errors): + """An argument path must be an ADDRESS: keyed throughout, and indexed exactly where + the builder iterates. + + The `[]` marker is the whole reason the path is structured rather than a string, so + the invariants are about where it may sit: + + * a path present but empty, or a segment with no `key` — the IR claims to know where + a value goes and then names nowhere; + * a CHILD's own path ends in `[]` if and only if the child repeats. That path + addresses the argument the combinator was handed, which is a list for + `REPEATED_CHILD` and a single object for `OPTIONAL_CHILD` / + `HAS_OPTIONAL_CHILD`; getting it backwards is precisely the confusion that writes + a value where the vendor builder never reads; + * an ATTRIBUTE's or CONTENT's path ends in `[]` only when it is the node's OWN path — + the value is then the list element itself, which is what a list of scalars looks + like (`REPEATED_CHILD(t, productIds)` with `t(v){ wap("id", null, v) }`). Any other + trailing `[]` on a value says the value is the whole array, which is the confusion + that writes `participantArgs.participantJid` where the builder reads + `participantArgs[0].participantJid`. + + Driven from the `argPath` ARRAY wherever it appears, so a node, an attribute and an + element content are all covered; a node is told apart by carrying a `tag`. The value + rule is checked from the NODE, because only there is the node's own path in hand. + """ + p = node.get("argPath") + if p is None: + return + if not isinstance(p, list) or not p: + errors.append(f"{path}/argPath: present but names no segment") + return + for i, seg in enumerate(p): + if not isinstance(seg, dict) or not seg.get("key"): + errors.append(f"{path}/argPath/{i}: segment with no key") + return + tail_is_list = bool(p[-1].get("list")) + if "tag" in node: + if tail_is_list and not node.get("repeats"): + errors.append( + f"{path}/argPath: ends in a list marker on a child that does not repeat" + ) + elif node.get("repeats") and not tail_is_list: + errors.append( + f"{path}/argPath: repeated child whose path does not address a list" + ) + values = [ + (f"{path}/attrs/{i}/argPath", a.get("argPath")) + for i, a in enumerate(node.get("attrs") or []) + ] + content = node.get("content") or {} + if content.get("argPath"): + values.append((f"{path}/content/argPath", content["argPath"])) + # A `const` payload is a literal the builder writes itself, so there is no + # argument to address. A `constBytes` one is *pinned* rather than written — + # every call site passes the same constant — so it keeps its address, and the + # two facts answer different questions. + if content.get("kind") == "const": + errors.append( + f"{path}/content/argPath: a literal the builder writes itself has no " + f"argument to address" + ) + for at, vp in values: + if not isinstance(vp, list) or not vp or not vp[-1].get("list"): + continue + if vp != p: + errors.append( + f"{at}: a value path ends in a list marker without being the node's " + f"own list — its last segment is read off an element, not off the array" + ) + + + +def check_request_child_cardinality(node, path, errors): + """A request child must not claim a cardinality it contradicts. + + `presence` and the repeat bounds describe the same call site, so the combinations + below are the document disagreeing with itself rather than an extraction gap: + + * a `presence_flag` child with attributes or content — the marker template takes no + arguments, so a consumer told to model it as a `bool` would silently drop them; + * `repeatMin`/`repeatMax` on a child that does not repeat; + * `repeatMin` greater than `repeatMax`, which admits nothing at all. + + Driven from the `children` ARRAY of a request node, which is the only place these + keys live; `presence` is skipped when required, so gating on it would have skipped + exactly the children this is about. + """ + children = node.get("children") + if not isinstance(children, list): + return + for i, c in enumerate(children): + if not isinstance(c, dict) or "tag" not in c: + continue + at = f"{path}/children/{i}" + if c.get("presence") == "presence_flag": + # A marker's template takes no arguments, so anything it writes is a constant + # the IR already carries — `` is still modelled as a bool + # plus a fixed payload, and rejecting it outright would block a regeneration + # over a perfectly consumable node. What cannot be true is a marker whose + # payload needs a VALUE, since the flag supplies none and nothing else can. + # Variant-group attributes count too: a marker whose disjunction carries a + # runtime attribute is exactly as unmodellable as one carrying it directly, + # and reading only `attrs` is how a guard passes by not looking. + variant_attrs = [ + a + for g in c.get("variantGroups") or [] + for v in g.get("variants") or [] + for a in v.get("attrs") or [] + ] + needy = [ + a.get("name") + for a in (c.get("attrs") or []) + variant_attrs + if a.get("kind") not in ("const", "generated_id") + ] + if needy: + errors.append( + f"{at}: presence marker reads argument(s) {needy} its boolean cannot " + f"supply" + ) + content = c.get("content") or {} + if content and content.get("kind") != "const" and "constBytes" not in content: + errors.append( + f"{at}: presence marker carries a runtime element value its boolean " + f"cannot supply" + ) + lo, hi = c.get("repeatMin"), c.get("repeatMax") + if (lo is not None or hi is not None) and not c.get("repeats"): + errors.append(f"{at}: repeat bounds on a child that does not repeat") + if lo is not None and hi is not None and lo > hi: + errors.append(f"{at}: repeatMin {lo} exceeds repeatMax {hi}") + + +def check_mixin_group_depth(node, path, domain, errors): + """A response field named `…MixinGroup` must carry the alternatives that make it one. + + WA composes most of a response out of mixins, and a `…MixinGroup` is the disjunction + over them — the group listing of `GetParticipatingGroups`, the key bundle of + `PreKeysFetchKeyBundles`. A field with that name and neither `unionVariants` nor + `children` is a leaf that looks like a scalar and is an entire union: a consumer + reads it as a string, gets nothing, and has no way to know what it missed. + + Named-based on purpose, and only here: this checks a NAME against the SHAPE the IR + already gave the field, rather than deriving anything from the name. + + Scoped to IQ response fields, which is the only place the construct exists. The + walker visits every object in every domain, so without the scope a request attribute, + an enum entry or a WAM field that happens to end in `MixinGroup` would be failed for + lacking keys its own shape never has. + """ + if domain != "iq" or "/response" not in path or "method" not in node: + return + name = node.get("name") + if not isinstance(name, str) or not name.endswith("MixinGroup"): + return + if node.get("unionVariants") or node.get("children"): + return + errors.append( + f"{path}: `{name}` is a union mixin group with no alternatives and no children — " + f"the extraction stopped at the mixin boundary" + ) + + def check_assertion(a, path, errors): if a.get("kind") == "reference": if not a.get("referencePath"): @@ -1404,6 +1642,9 @@ def visit(node, path, domain=domain): check_action_keys(node, f"{domain}{path}", errors, flattened) check_variant_groups(node, f"{domain}{path}", errors) check_child_requiredness(node, f"{domain}{path}", errors) + check_arg_path(node, f"{domain}{path}", errors) + check_request_child_cardinality(node, f"{domain}{path}", errors) + check_mixin_group_depth(node, f"{domain}{path}", domain, errors) walk(data, visit) # Needs the whole document, not one node: the reference and its definition sit in @@ -1418,6 +1659,8 @@ def visit(node, path, domain=domain): check_notif_identifiers(data, domain, errors) check_catalog_resolution(data, domain, catalog_keys, errors) count_unresolved_targets(data, domain, counts) + count_builder_gaps(data, domain, counts) + count_unaddressed_targets(data, domain, counts) collect_unresolved_enums(data, domain, proto_enums, unresolved) ok = True