From b5e78eb50e6768472e8988000cd179a106cfc929 Mon Sep 17 00:00:00 2001 From: Heyoub Date: Sat, 15 Aug 2026 13:20:30 -0400 Subject: [PATCH 1/5] Repair B1c and B2 foundation claims --- README.md | 5 +- src/02_identity/README.md | 5 +- src/13_declaration/README.md | 11 +- src/15_execution/types.rs | 4 +- src/laws.rs | 256 ++-- src/types.rs | 35 +- ...apacity-minted-for-an-undeclared-family.rs | 82 +- ...ity-minted-for-an-undeclared-family.stderr | 116 +- xtask/src/checks/alarms.rs | 54 +- xtask/src/checks/coupling.rs | 161 ++- xtask/src/checks/dependency.rs | 93 +- xtask/src/checks/hygiene.rs | 22 +- xtask/src/checks/obligations.rs | 4 +- xtask/src/checks/parity.rs | 12 +- xtask/src/checks/placement.rs | 77 +- xtask/src/checks/positivity.rs | 32 +- xtask/src/checks/scratch.rs | 124 +- xtask/src/checks/supply_chain.rs | 38 +- xtask/src/checks/toolchain.rs | 74 +- xtask/src/checks/vocabulary.rs | 46 +- xtask/src/main.rs | 34 +- xtask/src/repository/cargo.rs | 208 ++- xtask/src/repository/markdown.rs | 114 +- xtask/src/repository/mod.rs | 10 +- xtask/src/repository/snapshot.rs | 1284 +++++++++-------- xtask/src/repository/types.rs | 3 - 26 files changed, 1788 insertions(+), 1116 deletions(-) diff --git a/README.md b/README.md index b129ab1..ec38f28 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,9 @@ comparison would have to be settled, so its two facts are carried by values instead: `LimitWitness` is what schema validation selected, and `PositiveLimitWitness` is that selection proven to admit an item. A family says which ladder its magnitude travels by declaring `EvidenceSelectedLimit`, and -that declaration is the mint's bound — a family that never made it has no road -to a runtime capacity at all. Several families in the machine said +that declaration bounds the base witness type itself and every road consuming +it — a family that never made it cannot name a runtime witness or pass one to a +runtime-capacity road. Several families in the machine said "evidence-selected" in a doc comment beside their declaration and said it nowhere a road could read; that sentence is now a fact the compiler carries. diff --git a/src/02_identity/README.md b/src/02_identity/README.md index c1a38ed..ff949dd 100644 --- a/src/02_identity/README.md +++ b/src/02_identity/README.md @@ -121,8 +121,9 @@ containing dozens of other types*, and it is not answerable without being a compiler. Seating the guard in its own module answers it structurally. The set of roads is -the expansion, and nothing else is inside the wall — so `stamped-guards-seal- -their-position` is deleted rather than repaired, and the claim it used to make +the expansion, and nothing else is inside the wall — so +`stamped-guards-seal-their-position` is deleted rather than repaired, and the +claim it used to make is `E0616`, `E0423` and `E0603` on the two laundering fixtures. That is the drain running downward: a type that makes the wrong move unrepresentable retires the law that asserted the move was wrong, and the law goes. diff --git a/src/13_declaration/README.md b/src/13_declaration/README.md index b165069..6d168cc 100644 --- a/src/13_declaration/README.md +++ b/src/13_declaration/README.md @@ -46,11 +46,12 @@ These collections are bounded by a DECLARED finite issue bound, not roster cardinality — several issues of one kind are lawful at once — so their magnitudes are selected by the owner's evidence rather than written in the source. Those three families say so at the type level by declaring -`EvidenceSelectedLimit`, which is the bound on `PositiveLimitWitness`'s mint: the -sentence that used to sit in a doc comment beside each of them is now a fact a -road can read, and a family that never made the declaration has no road to a -runtime capacity. The projection-contract family alone is derivable (5 member -kinds + 5 unstatable claims = 10) and carries the compile-time cap instead. +`EvidenceSelectedLimit`, which bounds the base `LimitWitness` type and every +road consuming it: the sentence that used to sit in a doc comment beside each +of them is now a fact a road can read, and a family that never made the +declaration has no road to a runtime capacity. The projection-contract family +alone is derivable (5 member kinds + 5 unstatable claims = 10) and carries the +compile-time cap instead. All four bodies carry the one coupled seat — band 00's `AdmittedPrefix`, holding the issues and the coverage claim the same construction amounts to, read back diff --git a/src/15_execution/types.rs b/src/15_execution/types.rs index c188f14..9eaa0d0 100644 --- a/src/15_execution/types.rs +++ b/src/15_execution/types.rs @@ -705,8 +705,8 @@ pub enum EffectBatchCompositionIssue { }, } -/// Limit family for composition issues. Its magnitude is selected by the -/// owner's evidence rather than declared here — see +/// Limit family for `EffectBatch` composition issues. Its magnitude is +/// selected by the owner's evidence rather than declared here — see /// [`crate::types::EvidenceSelectedLimit`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EffectBatchIssueLimit; diff --git a/src/laws.rs b/src/laws.rs index c076b84..99481a6 100644 --- a/src/laws.rs +++ b/src/laws.rs @@ -64,8 +64,7 @@ mod root { } /// law: root.limit-families-do-not-unify — `Bounded` under one limit family is - /// a different type than under another, and a witness for one family cannot - /// authorize the other, regardless of magnitudes. + /// a different type than under another, regardless of magnitudes. /// Owed reversal (red twin): passing `Bounded` where /// `Bounded` is required must not compile. #[test] @@ -84,10 +83,8 @@ mod root { let decode_bounded: Option)> = Some(drop); let arena_bounded: Option)> = Some(drop); - let arena_witness: Option)> = Some(drop); assert!(decode_bounded.is_some()); assert!(arena_bounded.is_some()); - assert!(arena_witness.is_some()); assert_eq!(DecodeDemo::MAX, 8); } @@ -164,6 +161,11 @@ mod root { impl ConstLimit for SmallDemo { const MAX: usize = 2; } + struct RuntimeDemo; + impl Limit for RuntimeDemo { + type Authority = EvidenceSelectedMagnitude; + } + impl crate::types::EvidenceSelectedLimit for RuntimeDemo {} let admitted: AdmittedLimit = AdmittedLimit::under_profile(); let positive: PositiveLimit = @@ -175,10 +177,10 @@ mod root { Bounded::admitted_const(vec![1, 2, 3], &admitted); assert!(matches!(over, Err(BoundedConstruction::OverLimit))); - let witness: LimitWitness = LimitWitness::declared(1); - let witnessed: Result, _> = Bounded::admitted(vec![1], &witness); + let witness: LimitWitness = LimitWitness::declared(1); + let witnessed: Result, _> = Bounded::admitted(vec![1], &witness); assert!(witnessed.is_ok_and(|bounded| !bounded.is_empty())); - let over_witness: Result, _> = + let over_witness: Result, _> = Bounded::admitted(vec![1, 2], &witness); assert!(matches!(over_witness, Err(BoundedConstruction::OverLimit))); @@ -484,9 +486,9 @@ mod root { /// law: root.the-runtime-ladder-is-declared-by-its-family — a family reaches /// a runtime capacity only where its owner declared the magnitude - /// evidence-selected. The declaration is the MINT'S BOUND rather than a - /// sentence beside the family, so a family that never made it has no road to - /// a capacity at all. + /// evidence-selected. The declaration bounds the base witness type and + /// every road consuming it rather than standing as a sentence beside the + /// family, so a family that never made it has no road to a capacity at all. /// /// The green half is that the bound is real and satisfiable: a family /// declaring it reaches the mint, settled by the compiler over a function @@ -3355,11 +3357,7 @@ mod history { disposition: HistoryDisposition::Present(7), closure: SourceClosure(Completeness::Complete { over: SourceRegions { - regions: crate::types::Bounded::admitted( - vec![], - &crate::types::LimitWitness::declared(4), - ) - .unwrap_or_else(|_| unreachable!("empty fits")), + regions: crate::types::Bounded::empty(), }, }), freshness: crate::types::Freshness::Current(crate::types::Current::for_laws(7)), @@ -3490,8 +3488,8 @@ mod navigation { }; use crate::refusal::{FamilyShape, RefusalFamily}; use crate::types::{ - Bounded, Completeness, ConstLimit, EvidenceRef, Freshness, LimitWitness, - ReferentAvailability, ReferentIntegrity, Stale, + Bounded, Completeness, ConstLimit, EvidenceRef, Freshness, ReferentAvailability, + ReferentIntegrity, Stale, }; use core::cmp::Ordering; @@ -3526,8 +3524,7 @@ mod navigation { fn empty_regions() -> SourceRegions { SourceRegions { - regions: Bounded::admitted(vec![], &LimitWitness::declared(4)) - .unwrap_or_else(|_| unreachable!("empty fits")), + regions: Bounded::empty(), } } @@ -3565,8 +3562,7 @@ mod navigation { missing: empty_regions(), }), freshness: Freshness::Stale(Stale::for_laws(42, HistoryCut(demo_cut(5, 9)))), - alternatives: Bounded::admitted(vec![], &LimitWitness::declared(2)) - .unwrap_or_else(|_| unreachable!("empty fits")), + alternatives: Bounded::empty(), access: demo_evidence(6), provenance: demo_evidence(7), causation: demo_evidence(8), @@ -4281,18 +4277,17 @@ mod declaration { mod semantic { use super::pairwise_distinct; - use crate::bounds::{BoundClass, DimensionId}; use crate::declaration::Stage; use crate::identity::{Commitment, CreationLaw, IdentityClass, IdentityRole}; use crate::refusal::{FamilyShape, RefusalFamily}; use crate::semantic::{ - BehaviorFamily, BoundDimensionRow, CapabilityRequirements, DefinitionBoundary, - EvidenceObligation, ExplanationObligation, Judgment, OPERATION_CONTRACT_FACTS, - OrderedEffectRegions, RefusalSet, SEMANTIC_FORM_CONTENT, SemanticForm, - SemanticFormConstruction, SemanticFormConstructionIssue, SemanticGraphDigest, - SemanticTypeRef, SourceCutPosture, SymbolicBounds, + BehaviorFamily, CapabilityRequirements, DefinitionBoundary, EvidenceObligation, + ExplanationObligation, Judgment, OPERATION_CONTRACT_FACTS, OrderedEffectRegions, + RefusalSet, SEMANTIC_FORM_CONTENT, SemanticForm, SemanticFormConstruction, + SemanticFormConstructionIssue, SemanticGraphDigest, SemanticTypeRef, SourceCutPosture, + SymbolicBounds, }; - use crate::types::{Bounded, LimitWitness}; + use crate::types::Bounded; /// law: semantic.form-family-holds-fifteen — the content roster read as /// defects, every issue carrying only its canonical-order position, on @@ -4335,7 +4330,9 @@ mod semantic { /// law: semantic.judgment-binds-nine-axes — a complete judgment /// constructs with all nine typed members: normal type, stage, refusals, /// ordered effects, capability requirements, source posture, applicable - /// symbolic bounds, explanation, evidence. + /// symbolic bounds, explanation, evidence. Its bounded members are empty + /// here: their families declare no magnitude, so this structural law does + /// not claim a nonempty judgment population. /// Owed reversal (red twin): an erasable axis must not compile. #[test] fn judgment_binds_nine_axes() { @@ -4343,40 +4340,23 @@ mod semantic { normal_type: SemanticTypeRef(Commitment::raw([1; 32])), stage: Stage::Semantic, refuses: RefusalSet { - families: Bounded::admitted( - vec![Commitment::raw([2; 32])], - &LimitWitness::declared(4), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + families: Bounded::empty(), }, effects: OrderedEffectRegions { - regions: Bounded::admitted(vec![], &LimitWitness::declared(4)) - .unwrap_or_else(|_| unreachable!("empty fits")), + regions: Bounded::empty(), }, requires: CapabilityRequirements { - requirements: Bounded::admitted( - vec![Commitment::raw([3; 32])], - &LimitWitness::declared(4), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + requirements: Bounded::empty(), }, reads: SourceCutPosture(Commitment::raw([4; 32])), bounds: SymbolicBounds { - dimensions: Bounded::admitted( - vec![BoundDimensionRow { - dimension: DimensionId::registered(1), - class: BoundClass::Work, - maximum: 1_000, - }], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + dimensions: Bounded::empty(), }, explains: ExplanationObligation(Commitment::raw([5; 32])), evidences: EvidenceObligation(Commitment::raw([6; 32])), }; assert_eq!(judgment.stage, Stage::Semantic); - assert_eq!(judgment.bounds.dimensions.len(), 1); + assert!(judgment.bounds.dimensions.is_empty()); } /// law: semantic.behavior-and-boundary-rosters-hold — seven behavior @@ -4427,15 +4407,14 @@ mod semantic { mod execution { use super::pairwise_distinct; - use crate::bounds::{BoundClass, DimensionId}; use crate::execution::{ - AlgebraicLaw, AlgebraicLawLimit, CommandKind, CommandOrdinal, EffectBatch, - EffectBatchComposition, EffectCommand, EffectfulRecursionLane, ExecutionForm, - ExecutionFormConstruction, ExecutionFormConstructionIssue, ExecutionFormFamilyId, - ExecutionFormVersion, ForbiddenIdentitySource, GroupFenceDefect, - INDEPENDENCE_MAY_NOT_SHARE, INDEPENDENCE_MAY_SHARE, INTERLEAVED_CLOSURE_TOTALS, - KernelBindingPolicy, KernelBindingPolicyConstruction, KernelBindingPosture, - KernelFallbackPolicy, KernelInterfaceContract, KernelInterfaceContractConstructionIssue, + AlgebraicLaw, AlgebraicLawLimit, CommandKind, EffectBatch, EffectBatchComposition, + EffectfulRecursionLane, ExecutionForm, ExecutionFormConstruction, + ExecutionFormConstructionIssue, ExecutionFormFamilyId, ExecutionFormVersion, + ForbiddenIdentitySource, GroupFenceDefect, INDEPENDENCE_MAY_NOT_SHARE, + INDEPENDENCE_MAY_SHARE, INTERLEAVED_CLOSURE_TOTALS, KernelBindingPolicy, + KernelBindingPolicyConstruction, KernelBindingPosture, KernelFallbackPolicy, + KernelInterfaceContract, KernelInterfaceContractConstructionIssue, KernelInterfaceContractRef, KernelRealizationId, KernelRequirement, KernelSemanticContract, KernelSemanticContractConstructionIssue, KernelSemanticContractRef, KernelSubstitutionScope, OPERATOR_REGISTER, RecursionWitness, RequiredContractKind, @@ -4445,10 +4424,7 @@ mod execution { AuthorityPosition, Commitment, Occurrence, OccurrenceForm, OrderComparison, }; use crate::refusal::{FamilyShape, RefusalFamily}; - use crate::semantic::BoundDimensionRow; - use crate::types::{ - Bounded, ConstLimit, EvidenceRef, LimitWitness, ReferentAvailability, ReferentIntegrity, - }; + use crate::types::{Bounded, ConstLimit, EvidenceRef, ReferentAvailability, ReferentIntegrity}; use core::cmp::Ordering; fn demo_evidence(seed: u8) -> EvidenceRef { @@ -4540,36 +4516,22 @@ mod execution { assert_eq!(form.content(), &Commitment::raw([43; 32])); } - /// law: execution.effect-batch-composes-as-data — a real intent + /// law: execution.effect-batch-composes-as-data — the declaration shape /// constructs with no result member representable, and the composition - /// family holds its five issues with the three closed subcause rosters. + /// family holds its five issues with the three closed subcause rosters. The + /// bounded command and bound seats stay empty here rather than borrowing a + /// runtime witness their unstated families did not declare. /// Owed reversal (red twin): a result or receipt member must not compile. #[test] fn effect_batch_composes_as_data() { let batch = EffectBatch { - commands: Bounded::admitted( - vec![EffectCommand { - ordinal: CommandOrdinal::declared(0), - kind: CommandKind::EventAppend, - contracts: Commitment::raw([44; 32]), - }], - &LimitWitness::declared(16), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + commands: Bounded::empty(), boundary: Commitment::raw([45; 32]), groups_and_fences: Commitment::raw([46; 32]), idempotency: Commitment::raw([47; 32]), - bounds: Bounded::admitted( - vec![BoundDimensionRow { - dimension: DimensionId::registered(2), - class: BoundClass::Effect, - maximum: 8, - }], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + bounds: Bounded::empty(), }; - assert_eq!(batch.commands.len(), 1); + assert!(batch.commands.is_empty()); let kinds = [ CommandKind::EventAppend, CommandKind::EffectIntentAdmission, @@ -4728,7 +4690,7 @@ mod image { ImageValidation, PROGRAM_IMAGE_EXTENSION, PackagingProfile, ProgramImage, ProgramImageComponent, SemanticImage, UntrustedImageBytes, }; - use crate::types::{Bounded, LimitWitness}; + use crate::types::Bounded; use core::cmp::Ordering; /// law: image.identities-ride-scope-guards — family-format, profile, and @@ -4820,8 +4782,11 @@ mod image { assert!(matches!(component.carriage, ComponentCarriage::Inline)); } - /// law: image.program-image-composes — a real image constructs through - /// the checked roads, and the bound-fact roster holds eighteen. + /// law: image.program-image-composes — the image carrier binds its family, + /// profile, packaging, and bounded component/kernel seats, and the + /// bound-fact roster holds eighteen. This laws-only value leaves both + /// bounded seats empty rather than pretending validation supplied their + /// unstated magnitudes; dual-form admission remains owed below. /// Owed reversal: an image without both forms' components must refuse at /// validation (owed to the machinery seam). #[test] @@ -4836,23 +4801,13 @@ mod image { 1, )), packaging: PackagingProfile::SelfContained, - components: Bounded::admitted( - vec![ProgramImageComponent { - role: ComponentRole::ExecutionForm, - profile: 1, - content: ContentRegionId::of(ByteIdentity::raw([68; 32])), - length: 1_024, - carriage: ComponentCarriage::Inline, - }], - &LimitWitness::declared(32), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + components: Bounded::empty(), kernel_requirements: KernelRequirementSet { - requirements: Bounded::admitted(vec![], &LimitWitness::declared(8)) - .unwrap_or_else(|_| unreachable!("empty fits")), + requirements: Bounded::empty(), }, }; assert!(matches!(image.packaging, PackagingProfile::SelfContained)); + assert!(image.components.is_empty()); assert_eq!(BOUND_FACT_ROSTER.len(), 18); assert_eq!(PROGRAM_IMAGE_EXTENSION, ".program.tpk"); } @@ -4902,18 +4857,14 @@ mod image { mod pakvm { use super::pairwise_distinct; - use crate::bounds::{BoundClass, DimensionId}; use crate::identity::Commitment; use crate::pakvm::{ ArenaIndex, CLOSURE_OBLIGATIONS, CapabilityHandle, CaptureRecord, ContinuationRecord, INVALID_CAPTURES, LambdaBoundaryPosture, PROHIBITED_INHABITANTS, PortHandle, ReplyHandle, STEP_PRODUCTIONS, ValueCategory, ValueResidence, VmTerminal, }; - use crate::semantic::BoundDimensionRow; - use crate::time::{ConsumedBudgetEvidence, RecordingSite, SpendRecord}; - use crate::types::{ - Bounded, EvidenceRef, LimitWitness, ReferentAvailability, ReferentIntegrity, - }; + use crate::time::{ConsumedBudgetEvidence, RecordingSite}; + use crate::types::{Bounded, EvidenceRef, ReferentAvailability, ReferentIntegrity}; fn demo_evidence(seed: u8) -> EvidenceRef { EvidenceRef::bound( @@ -4974,7 +4925,9 @@ mod pakvm { /// law: pakvm.continuation-record-binds-twelve — a real persisted /// continuation constructs with all members, carrying the deadline-policy /// reference plus consumed-budget evidence — never a live monotonic - /// value. + /// value. The remaining-bound and spend seats are empty here rather than + /// inventing magnitudes for their unstated families; nonempty budget + /// behavior remains gated. /// Owed reversal (red twin): a live monotonic member must not compile /// (the live deadline type is unserializable and `!Send` by shape). #[test] @@ -4988,28 +4941,12 @@ mod pakvm { effect_intent: demo_evidence(74), attempt: demo_evidence(75), generations: Commitment::raw([76; 32]), - remaining_bounds: Bounded::admitted( - vec![BoundDimensionRow { - dimension: DimensionId::registered(3), - class: BoundClass::Suspension, - maximum: 2, - }], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + remaining_bounds: Bounded::empty(), deadline_policy: demo_evidence(77), spend: ConsumedBudgetEvidence { site: RecordingSite::EffectAttempt, coordinate: demo_evidence(78), - spends: Bounded::admitted( - vec![SpendRecord { - dimension: DimensionId::registered(4), - magnitude: 100, - uncertainty: 5, - }], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + spends: Bounded::empty(), }, posture: Commitment::raw([79; 32]), }; @@ -5037,22 +4974,19 @@ mod pakvm { assert_eq!(STEP_PRODUCTIONS.first(), Some(&"semantic-value")); } - /// law: pakvm.captures-and-closure-obligations — the capture record - /// constructs in canonical binding order, the seven invalid captures and - /// four lambda postures hold, and the six closure obligations stand. + /// law: pakvm.captures-and-closure-obligations — the capture record carries + /// its bounded canonical-order seat, the seven invalid captures and four + /// lambda postures hold, and the six closure obligations stand. This + /// specimen is the lawful empty capture set; ordering hostiles remain owed. /// Owed reversal (red twin): a captured live handle must not compile. #[test] fn captures_and_closure_obligations() { let record = CaptureRecord { definition: Commitment::raw([80; 32]), - captures: Bounded::admitted( - vec![Commitment::raw([81; 32])], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + captures: Bounded::empty(), origins: demo_evidence(82), }; - assert_eq!(record.captures.len(), 1); + assert!(record.captures.is_empty()); assert_eq!(INVALID_CAPTURES.len(), 7); let postures = [ LambdaBoundaryPosture::InlineOnly, @@ -5085,10 +5019,7 @@ mod bvisor { use crate::identity::{AuthorityPosition, Commitment, Occurrence, OccurrenceForm}; use crate::port::{PortFamilyId, PortFamilyVersion, PortPostcondition}; use crate::refusal::{FamilyShape, RefusalFamily}; - use crate::semantic::BoundDimensionRow; - use crate::types::{ - Bounded, EvidenceRef, LimitWitness, ReferentAvailability, ReferentIntegrity, - }; + use crate::types::{Bounded, EvidenceRef, ReferentAvailability, ReferentIntegrity}; fn demo_observation(seed: u8) -> ReservationObservation { ReservationObservation { @@ -5283,7 +5214,8 @@ mod bvisor { /// law: bvisor.port-crossing-binds — a real port request constructs /// bound to one Attempt and one family; the validation and cancellation - /// rosters hold. + /// rosters hold. Its bounded-request seat is empty here because the family + /// declares no magnitude; this law does not claim a nonempty bound set. /// Owed reversal (red twin): a request satisfying another Attempt must /// not compile. #[test] @@ -5298,17 +5230,9 @@ mod bvisor { 1, )), payload: Commitment::raw([104; 32]), - bounds: Bounded::admitted( - vec![BoundDimensionRow { - dimension: DimensionId::registered(6), - class: crate::bounds::BoundClass::Work, - maximum: 500, - }], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + bounds: Bounded::empty(), }; - assert_eq!(request.bounds.len(), 1); + assert!(request.bounds.is_empty()); assert_eq!(PORT_REQUEST_VALIDATION.len(), 10); assert_eq!(CANCELLATION_FACTS.len(), 10); } @@ -5333,9 +5257,7 @@ mod runtime { RECOVERY_ACTIONS, ReconciliationDisposition, ReconciliationLifecycle, ReplayPosture, STITCH_OUTPUTS, SemanticRecoveryAuthority, TURN_PREIMAGE, TurnId, TurnPhase, }; - use crate::types::{ - Bounded, EvidenceRef, LimitWitness, ReferentAvailability, ReferentIntegrity, - }; + use crate::types::{Bounded, EvidenceRef, ReferentAvailability, ReferentIntegrity}; fn demo_evidence(seed: u8) -> EvidenceRef { EvidenceRef::bound( @@ -5394,8 +5316,10 @@ mod runtime { } /// law: runtime.attempt-lineage-is-message-passing — the three-way cause - /// sum binds one endpoint each, the cause set is bounded membership, and - /// a lineage node composes with the membrane's Attempt identity. + /// sum binds one endpoint each, the cause set carries a bounded membership + /// seat, and a lineage node composes with the membrane's Attempt identity. + /// The node's seat stays empty here rather than inventing its unstated + /// family's magnitude. /// Owed reversal (red twin): a bare cause or an edge inside the cause /// value must not compile. #[test] @@ -5414,14 +5338,10 @@ mod runtime { let node = AttemptLineageNode { attempt: AttemptId::for_laws(Occurrence::for_laws(OccurrenceForm::Fresh([113; 16]))), causes: BoundedCauseSet { - causes: Bounded::admitted( - vec![AttemptCause::Turn(turn)], - &LimitWitness::declared(4), - ) - .unwrap_or_else(|_| unreachable!("one fits")), + causes: Bounded::empty(), }, }; - assert_eq!(node.causes.causes.len(), 1); + assert!(node.causes.causes.is_empty()); } /// law: runtime.checkpoint-advances-only-on-prerequisites — the durable @@ -6228,7 +6148,7 @@ mod evidence { }; use crate::identity::Commitment; use crate::types::{ - Bounded, Completeness, EvidenceRef, LimitWitness, ProofDisposition, ReferentAvailability, + Bounded, Completeness, EvidenceRef, ProofDisposition, ReferentAvailability, ReferentIntegrity, }; @@ -6405,8 +6325,10 @@ mod evidence { /// law: evidence.receipt-matrix-and-carriage — twenty-five families by /// semantic boundary (the two structurally two-record rows named), - /// per-item carriage, coexisting commitment layers, and the calibration - /// pair owning neither work nor truth. + /// per-item carriage, the bounded commitment-layer seat, and the + /// calibration pair owning neither work nor truth. The layer specimen is + /// empty because its family declares no magnitude; coexistence behavior + /// remains outside this compile-time structural law. /// Owed reversal (red twin): a universal receipt must not exist. #[test] fn receipt_matrix_and_carriage() { @@ -6419,13 +6341,9 @@ mod evidence { ]; assert_eq!(carriage.len(), 2); let layers = CommitmentLayers { - layers: Bounded::admitted( - vec![Commitment::raw([214; 32]), Commitment::raw([215; 32])], - &LimitWitness::declared(8), - ) - .unwrap_or_else(|_| unreachable!("two fit")), + layers: Bounded::empty(), }; - assert_eq!(layers.layers.len(), 2); + assert!(layers.layers.is_empty()); let calibration = CalibrationEvidence { model: CalibrationModel(Commitment::raw([216; 32])), evidence: demo_evidence(217), diff --git a/src/types.rs b/src/types.rs index 5f509dc..f94471d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -156,8 +156,8 @@ pub trait Limit { /// [`DeclaredMagnitude`] without implementing [`ConstLimit`] leaves a family /// with no `MAX` and therefore no road to [`AdmittedLimit`]; declaring /// [`EvidenceSelectedMagnitude`] without implementing - /// [`EvidenceSelectedLimit`] leaves it with no road to - /// [`PositiveLimitWitness`]. Both are inert rather than wrong: the family + /// [`EvidenceSelectedLimit`] leaves it with no road to either [`LimitWitness`] + /// or [`PositiveLimitWitness`]. Both are inert rather than wrong: the family /// names a road and never walks it, and the ladder traits stay the one place /// a capacity is actually reachable from. type Authority: CapacityAuthority; @@ -191,9 +191,9 @@ pub trait ConstLimit: Limit { /// evidence-selected" in PROSE beside their declaration and said it nowhere a /// road could read, which left the second ladder with a magnitude nothing /// carried and a positivity nothing established. This trait is where that -/// sentence becomes a fact the compiler carries: [`PositiveLimitWitness`]'s mint -/// is bounded on it, so a family that never declared its magnitude -/// evidence-selected has no road to a runtime capacity at all. +/// sentence becomes a fact the compiler carries: both runtime witness types and +/// every road consuming them are bounded on it, so a family that never declared +/// its magnitude evidence-selected has no road to a runtime capacity at all. /// /// # What implementing it claims, and what it does not /// @@ -457,6 +457,13 @@ impl PositiveLimit { /// witness up, in [`PositiveLimitWitness`], where exactly the runtime roads /// promising an inhabitant consume it. /// +/// The family bound is part of this BASE rung rather than deferred to the +/// positive one: a runtime-selected magnitude cannot even be named for a +/// family whose owner did not declare [`EvidenceSelectedLimit`]. Consequently +/// every mint and consumer inherits the same authority fact from the witness +/// it must mention, and no weaker base value can be handed to +/// [`Bounded::admitted`] under a declared or unstated family. +/// /// # No production road mints one, and that is stated rather than implied /// /// `LimitWitness::declared` is the only mint, it is `#[cfg(test)]`, and it is @@ -474,12 +481,12 @@ impl PositiveLimit { /// documentation build does not contain it. #[must_use = "a limit witness is the magnitude schema validation established; dropping it \ discards the only admitted bound for its family"] -pub struct LimitWitness { +pub struct LimitWitness { max: usize, _family: PhantomData, } -impl LimitWitness { +impl LimitWitness { /// In-crate mint for laws. Test-gated until the schema home carries the real /// declaration path — the gate comes off when a lawful minter exists. #[cfg(test)] @@ -581,7 +588,7 @@ pub enum CapacityAdmission { #[must_use = "a positive limit witness is the evidence a family's evidence-selected magnitude \ admits an item; dropping it discards the only proof a runtime road promising an \ inhabitant may act on"] -pub struct PositiveLimitWitness { +pub struct PositiveLimitWitness { witness: LimitWitness, } @@ -619,7 +626,7 @@ impl PositiveLimitWitness { } } -impl PositiveLimitWitness { +impl PositiveLimitWitness { /// The witnessed maximum this witness carries; at least one by construction. /// /// Read off the contained base witness, so no second copy of the magnitude @@ -771,7 +778,10 @@ impl Bounded { /// /// Returns [`BoundedConstruction::OverLimit`] when the items exceed the /// witnessed maximum. - pub fn admitted(items: Vec, witness: &LimitWitness) -> Result { + pub fn admitted(items: Vec, witness: &LimitWitness) -> Result + where + L: EvidenceSelectedLimit, + { if items.len() <= witness.max() { Ok(Self { items, @@ -1035,7 +1045,10 @@ impl NonEmptyBounded { first: T, rest: Vec, witness: &PositiveLimitWitness, - ) -> Result { + ) -> Result + where + L: EvidenceSelectedLimit, + { if rest.len().saturating_add(1) <= witness.max() { Ok(Self { first, diff --git a/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs b/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs index 18c2d84..0e5885c 100644 --- a/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs +++ b/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs @@ -1,15 +1,16 @@ -//! The reversal for the runtime ladder's gate: a family whose owner never -//! declared its magnitude evidence-selected has no road to a runtime capacity. +//! The reversal for the runtime ladder's gate: a family whose owner did not +//! declare its magnitude evidence-selected cannot even name the base runtime +//! witness, so it has no value to mint or pass to a runtime-capacity road. //! -//! The two families below differ in exactly one line. Both are limit families; -//! one declares `EvidenceSelectedLimit` and the other does not, and nothing else -//! about them differs — same shape, same absence of a compile-time magnitude, -//! same seat. So the refusal below can only be the missing declaration, and the -//! lawful half above it is what says the bound is satisfiable at all. +//! The selected family is the positive control. Beside it stand both other +//! authority states: one family supplies a source-declared magnitude and one +//! leaves its magnitude unstated. Neither is evidence-selected, and both stop +//! at `LimitWitness`'s own bound before a consumer can receive a value. //! -//! Nothing is minted here either. The bound sits on the mint, so naming the mint -//! as a value is enough to make the compiler settle it, and a fixture outside -//! this crate has no `LimitWitness` to build in any case. +//! Nothing is minted here. That is the stronger test: a fixture outside this +//! crate has no public mint, while the type bound proves that even a future +//! owner mint cannot produce the wrong authority and that +//! `Bounded::admitted` cannot name such a witness in its signature. //! //! # The recorded diagnostic carries a population nobody wrote //! @@ -23,42 +24,61 @@ //! from the sources is still owed. use threadpak::types::{ - CapacityAdmission, EvidenceSelectedLimit, EvidenceSelectedMagnitude, Limit, LimitWitness, - PositiveLimitWitness, UnstatedMagnitude, + Bounded, BoundedConstruction, ConstLimit, DeclaredMagnitude, EvidenceSelectedLimit, + EvidenceSelectedMagnitude, Limit, LimitWitness, UnstatedMagnitude, }; /// A family whose owner declared the magnitude evidence-selected. -struct DeclaredFamily; +struct SelectedFamily; -impl Limit for DeclaredFamily { +impl Limit for SelectedFamily { type Authority = EvidenceSelectedMagnitude; } -impl EvidenceSelectedLimit for DeclaredFamily {} +impl EvidenceSelectedLimit for SelectedFamily {} + +/// A family whose owner supplies a source-declared magnitude instead. +struct SourceDeclaredFamily; + +impl Limit for SourceDeclaredFamily { + type Authority = DeclaredMagnitude; +} + +impl ConstLimit for SourceDeclaredFamily { + const MAX: usize = 8; +} /// A family whose owner did not. It is a lawful limit family and bounds seats /// like any other; what it has not done is admit the runtime ladder. -struct UndeclaredFamily; +struct UnstatedFamily; -impl Limit for UndeclaredFamily { +impl Limit for UnstatedFamily { type Authority = UnstatedMagnitude; } -/// The lawful half, and it must stay lawful: the declared family reaches the -/// mint. -const DECLARED: fn( - LimitWitness, -) -> Result, CapacityAdmission> = - PositiveLimitWitness::inhabited; +/// The lawful half: a selected-family witness can be named at the consumer. +fn selected( + witness: &LimitWitness, +) -> Result, BoundedConstruction> { + Bounded::admitted(Vec::new(), witness) +} + +/// The source-declared ladder cannot name or pass the runtime witness. +fn source_declared( + witness: &LimitWitness, +) -> Result, BoundedConstruction> { + Bounded::admitted(Vec::new(), witness) +} -/// The unlawful half: the same mint, named for a family that never declared the -/// ladder it belongs to. -const UNDECLARED: fn( - LimitWitness, -) -> Result, CapacityAdmission> = - PositiveLimitWitness::inhabited; +/// The unstated family cannot name or pass the runtime witness either. +fn unstated( + witness: &LimitWitness, +) -> Result, BoundedConstruction> { + Bounded::admitted(Vec::new(), witness) +} fn main() { - let _ = DECLARED; - let _ = UNDECLARED; + let _ = selected; + let _ = source_declared; + let _ = unstated; } diff --git a/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.stderr b/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.stderr index fbe4230..f2f72c6 100644 --- a/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.stderr +++ b/testpak/tests/compile-fail/a-capacity-minted-for-an-undeclared-family.stderr @@ -1,29 +1,117 @@ -error[E0277]: the trait bound `UndeclaredFamily: EvidenceSelectedLimit` is not satisfied - --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:59:5 +error[E0277]: the trait bound `SourceDeclaredFamily: EvidenceSelectedLimit` is not satisfied + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:68:15 | -59 | PositiveLimitWitness::inhabited; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound +68 | witness: &LimitWitness, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | -help: the trait `EvidenceSelectedLimit` is not implemented for `UndeclaredFamily` +help: the trait `EvidenceSelectedLimit` is not implemented for `SourceDeclaredFamily` --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:41:1 | -41 | struct UndeclaredFamily; - | ^^^^^^^^^^^^^^^^^^^^^^^ +41 | struct SourceDeclaredFamily; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: the following other types implement trait `EvidenceSelectedLimit`: AdmissionIssueLimit AuthoredNameIssueLimit ClosureNamespaceIssueLimit - DeclaredFamily EffectBatchIssueLimit ExecutionFormIssueLimit LinkResolutionIssueLimit + SelectedFamily SemanticFormIssueLimit ShredParticipantLimit -note: required by a bound in `PositiveLimitWitness::::inhabited` +note: required by a bound in `LimitWitness` --> $WORKSPACE/src/types.rs | - | impl PositiveLimitWitness { - | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PositiveLimitWitness::::inhabited` -... - | pub fn inhabited(witness: LimitWitness) -> Result { - | --------- required by a bound in this associated function + | pub struct LimitWitness { + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `LimitWitness` + +error[E0277]: the trait bound `UnstatedFamily: EvidenceSelectedLimit` is not satisfied + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:75:15 + | +75 | witness: &LimitWitness, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `EvidenceSelectedLimit` is not implemented for `UnstatedFamily` + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:53:1 + | +53 | struct UnstatedFamily; + | ^^^^^^^^^^^^^^^^^^^^^ + = help: the following other types implement trait `EvidenceSelectedLimit`: + AdmissionIssueLimit + AuthoredNameIssueLimit + ClosureNamespaceIssueLimit + EffectBatchIssueLimit + ExecutionFormIssueLimit + LinkResolutionIssueLimit + SelectedFamily + SemanticFormIssueLimit + ShredParticipantLimit +note: required by a bound in `LimitWitness` + --> $WORKSPACE/src/types.rs + | + | pub struct LimitWitness { + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `LimitWitness` + +error[E0277]: the trait bound `SourceDeclaredFamily: EvidenceSelectedLimit` is not satisfied + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:70:35 + | +70 | Bounded::admitted(Vec::new(), witness) + | ----------------- ^^^^^^^ unsatisfied trait bound + | | + | required by a bound introduced by this call + | +help: the trait `EvidenceSelectedLimit` is not implemented for `SourceDeclaredFamily` + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:41:1 + | +41 | struct SourceDeclaredFamily; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: the following other types implement trait `EvidenceSelectedLimit`: + AdmissionIssueLimit + AuthoredNameIssueLimit + ClosureNamespaceIssueLimit + EffectBatchIssueLimit + ExecutionFormIssueLimit + LinkResolutionIssueLimit + SelectedFamily + SemanticFormIssueLimit + ShredParticipantLimit +note: required by a bound in `threadpak::types::Bounded::::admitted` + --> $WORKSPACE/src/types.rs + | + | pub fn admitted(items: Vec, witness: &LimitWitness) -> Result + | -------- required by a bound in this associated function + | where + | L: EvidenceSelectedLimit, + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Bounded::::admitted` + +error[E0277]: the trait bound `UnstatedFamily: EvidenceSelectedLimit` is not satisfied + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:77:35 + | +77 | Bounded::admitted(Vec::new(), witness) + | ----------------- ^^^^^^^ unsatisfied trait bound + | | + | required by a bound introduced by this call + | +help: the trait `EvidenceSelectedLimit` is not implemented for `UnstatedFamily` + --> tests/compile-fail/a-capacity-minted-for-an-undeclared-family.rs:53:1 + | +53 | struct UnstatedFamily; + | ^^^^^^^^^^^^^^^^^^^^^ + = help: the following other types implement trait `EvidenceSelectedLimit`: + AdmissionIssueLimit + AuthoredNameIssueLimit + ClosureNamespaceIssueLimit + EffectBatchIssueLimit + ExecutionFormIssueLimit + LinkResolutionIssueLimit + SelectedFamily + SemanticFormIssueLimit + ShredParticipantLimit +note: required by a bound in `threadpak::types::Bounded::::admitted` + --> $WORKSPACE/src/types.rs + | + | pub fn admitted(items: Vec, witness: &LimitWitness) -> Result + | -------- required by a bound in this associated function + | where + | L: EvidenceSelectedLimit, + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Bounded::::admitted` diff --git a/xtask/src/checks/alarms.rs b/xtask/src/checks/alarms.rs index 25b7a8a..41b8e93 100644 --- a/xtask/src/checks/alarms.rs +++ b/xtask/src/checks/alarms.rs @@ -279,13 +279,13 @@ mod tests { const WORKFLOW_FIXTURE: &str = "name: harness\n"; /// One scratch root carrying both alarms whole. - fn planted(name: &str) -> Scratch { - let scratch = Scratch::named(name); - scratch.write(&spelled(&HARNESS_WORKFLOW), WORKFLOW_FIXTURE); - scratch.write(&spelled(&MUTATION_WORKFLOW), WORKFLOW_FIXTURE); - scratch.write(&spelled(&HARNESS_CONFIGURATION), HARNESS_FIXTURE); - scratch.write(&spelled(&MUTATION_CONFIGURATION), MUTATION_FIXTURE); - scratch + fn planted(name: &str) -> Result { + let scratch = Scratch::named(name)?; + scratch.write(&spelled(&HARNESS_WORKFLOW), WORKFLOW_FIXTURE)?; + scratch.write(&spelled(&MUTATION_WORKFLOW), WORKFLOW_FIXTURE)?; + scratch.write(&spelled(&HARNESS_CONFIGURATION), HARNESS_FIXTURE)?; + scratch.write(&spelled(&MUTATION_CONFIGURATION), MUTATION_FIXTURE)?; + Ok(scratch) } /// The positive control: both alarms carrying all four artifacts, with a @@ -294,7 +294,7 @@ mod tests { /// every reversal below and be worthless. #[test] fn present_and_distinct_artifacts_are_lawful() -> Result<(), String> { - let scratch = planted("alarms-whole"); + let scratch = planted("alarms-whole")?; let found = check_alarm_artifacts(&scratch.read()?); assert!(found.is_ok(), "{found:?}"); Ok(()) @@ -305,8 +305,8 @@ mod tests { /// moved no number and failed no stage. #[test] fn a_deleted_harness_configuration_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-harness-configuration-deleted"); - scratch.remove(&spelled(&HARNESS_CONFIGURATION)); + let scratch = planted("alarms-harness-configuration-deleted")?; + scratch.remove(&spelled(&HARNESS_CONFIGURATION))?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains(".config/nextest.toml") @@ -321,8 +321,8 @@ mod tests { /// finds nothing, and exits zero. #[test] fn a_deleted_mutation_configuration_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-mutation-configuration-deleted"); - scratch.remove(&spelled(&MUTATION_CONFIGURATION)); + let scratch = planted("alarms-mutation-configuration-deleted")?; + scratch.remove(&spelled(&MUTATION_CONFIGURATION))?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains(".cargo/mutants.toml") @@ -336,8 +336,8 @@ mod tests { /// no committed file names a run for. #[test] fn a_deleted_harness_workflow_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-harness-workflow-deleted"); - scratch.remove(&spelled(&HARNESS_WORKFLOW)); + let scratch = planted("alarms-harness-workflow-deleted")?; + scratch.remove(&spelled(&HARNESS_WORKFLOW))?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("harness.yml") @@ -350,8 +350,8 @@ mod tests { /// Planted reversal: the mutation workflow deleted. #[test] fn a_deleted_mutation_workflow_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-mutation-workflow-deleted"); - scratch.remove(&spelled(&MUTATION_WORKFLOW)); + let scratch = planted("alarms-mutation-workflow-deleted")?; + scratch.remove(&spelled(&MUTATION_WORKFLOW))?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("mutation.yml") @@ -368,11 +368,11 @@ mod tests { /// decoder reads as the empty document it is. #[test] fn an_emptied_mutation_configuration_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-mutation-configuration-emptied"); + let scratch = planted("alarms-mutation-configuration-emptied")?; scratch.write( &spelled(&MUTATION_CONFIGURATION), "# every decision that was here is gone\n", - ); + )?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("states no key at all")), @@ -386,11 +386,11 @@ mod tests { /// thing by which this file can be shown to be READ is gone. #[test] fn a_harness_configuration_with_no_reversal_profile_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-reversal-profile-deleted"); + let scratch = planted("alarms-reversal-profile-deleted")?; scratch.write( &spelled(&HARNESS_CONFIGURATION), "[profile.default]\nfail-fast = false\n", - ); + )?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("does not state both")), @@ -409,11 +409,11 @@ mod tests { /// wrong on purpose — has quietly stopped being true of it. #[test] fn a_reversal_profile_that_stopped_departing_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-reversal-profile-synchronized"); + let scratch = planted("alarms-reversal-profile-synchronized")?; scratch.write( &spelled(&HARNESS_CONFIGURATION), "[profile.default]\nfail-fast = false\n\n[profile.reversal]\nfail-fast = false\n", - ); + )?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("has been synchronized")), @@ -428,11 +428,11 @@ mod tests { /// name. #[test] fn an_emptied_reversal_profile_is_a_violation() -> Result<(), String> { - let scratch = planted("alarms-reversal-profile-emptied"); + let scratch = planted("alarms-reversal-profile-emptied")?; scratch.write( &spelled(&HARNESS_CONFIGURATION), "[profile.default]\nfail-fast = false\n\n[profile.reversal]\n", - ); + )?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("states nothing")), @@ -453,10 +453,10 @@ mod tests { /// deletion is the line of the diff where the claim is allowed to grow back. #[test] fn workflows_that_invoke_nothing_still_pass() -> Result<(), String> { - let scratch = planted("alarms-workflows-invoke-nothing"); + let scratch = planted("alarms-workflows-invoke-nothing")?; let empty = "name: harness\non:\n schedule:\n - cron: '0 0 * * 0'\njobs: {}\n"; - scratch.write(&spelled(&HARNESS_WORKFLOW), empty); - scratch.write(&spelled(&MUTATION_WORKFLOW), empty); + scratch.write(&spelled(&HARNESS_WORKFLOW), empty)?; + scratch.write(&spelled(&MUTATION_WORKFLOW), empty)?; let found = check_alarm_artifacts(&scratch.read()?); assert!( found.is_ok(), diff --git a/xtask/src/checks/coupling.rs b/xtask/src/checks/coupling.rs index 8665c91..5a1789b 100644 --- a/xtask/src/checks/coupling.rs +++ b/xtask/src/checks/coupling.rs @@ -264,7 +264,13 @@ fn read_sources(sources: &[(&CanonicalPath, &syn::File)]) -> Reading { }; for (path, file) in sources { let spelled = path.as_str(); - read_module(spelled, &declaring_home(spelled), &file.items, &mut reading); + read_module( + spelled, + &declaring_home(spelled), + spelled, + &file.items, + &mut reading, + ); } reading } @@ -286,15 +292,23 @@ fn declaring_home(path: &str) -> String { /// Reads one module's items, then every inline module inside it. /// -/// An inline `mod` is its own resolution scope, keyed by the file that writes it -/// so two files' identically named inline modules never resolve into each other. +/// An inline `mod` is its own resolution scope, keyed by the file and the full +/// inline-module chain that writes it. Two files' identically named modules do +/// not resolve into each other, and neither do two same-named children under +/// distinct parents in one file. /// Written as an `if let` chain rather than a match because `syn::Item` is /// `non_exhaustive`: the items this reading has a question about are named, and /// every other item is passed over without a wildcard arm standing in for a set /// no crate outside `syn` can enumerate. The expectation that used to sit here — /// naming the lint and the reason — was a real refusal of a real hatch, and it /// is gone because the shape that needed it is gone. -fn read_module(path: &str, home: &str, items: &[syn::Item], reading: &mut Reading) { +fn read_module( + path: &str, + home: &str, + inline_scope: &str, + items: &[syn::Item], + reading: &mut Reading, +) { for item in items { if let syn::Item::Impl(declared) = item { if let Some(family) = collection_family(declared) { @@ -313,7 +327,7 @@ fn read_module(path: &str, home: &str, items: &[syn::Item], reading: &mut Readin }); } } else if let syn::Item::Mod(module) = item { - read_inline_module(path, home, module, items, reading); + read_inline_module(path, home, inline_scope, module, items, reading); } } } @@ -329,6 +343,7 @@ fn read_module(path: &str, home: &str, items: &[syn::Item], reading: &mut Readin fn read_inline_module( path: &str, home: &str, + inline_scope: &str, module: &syn::ItemMod, siblings: &[syn::Item], reading: &mut Reading, @@ -336,8 +351,8 @@ fn read_inline_module( let Some((_, inner)) = &module.content else { return; }; - let inside = format!("{path}::{}", module.ident); - read_module(path, &inside, inner, reading); + let inside = format!("{inline_scope}::{}", module.ident); + read_module(path, &inside, &inside, inner, reading); for name in reexported_from(&module.ident, siblings) { read_reexported_body(home, &name, inner, reading); } @@ -352,15 +367,39 @@ fn reexported_from(child: &syn::Ident, items: &[syn::Item]) -> Vec { let mut names = Vec::new(); for item in items { if let syn::Item::Use(declared) = item - && let syn::UseTree::Path(rooted) = &declared.tree - && rooted.ident == *child + && let Some(tree) = immediate_child_reexport(&declared.tree, child) { - reexported_names(&rooted.tree, &mut names); + reexported_names(tree, &mut names); } } names } +/// The tree re-exported from one immediate child. +/// +/// Exactly one leading `self::` is normalized because it names the same child +/// from the enclosing module. No other prefix is followed: `super`, `crate`, +/// aliases, and repeated paths would require name resolution this reader does +/// not own. +fn immediate_child_reexport<'tree>( + tree: &'tree syn::UseTree, + child: &syn::Ident, +) -> Option<&'tree syn::UseTree> { + let syn::UseTree::Path(rooted) = tree else { + return None; + }; + if rooted.ident == *child { + return Some(&rooted.tree); + } + if rooted.ident != "self" { + return None; + } + let syn::UseTree::Path(rooted) = &*rooted.tree else { + return None; + }; + (rooted.ident == *child).then_some(&rooted.tree) +} + /// Every name one `use` tree brings in, under the spelling the enclosing module /// then knows it by. fn reexported_names(tree: &syn::UseTree, into: &mut Vec) { @@ -581,6 +620,52 @@ mod tests { assert!(verdict.offenders.is_empty(), "{:?}", verdict.offenders); } + /// One leading `self::` names the same immediate child from the enclosing + /// module. The bounded lift normalizes that spelling without attempting + /// general import resolution. + #[test] + fn a_self_qualified_re_export_resolves_the_immediate_child() { + let verdict = coupled_body_verdict(&source( + "pub use self::seat::DemoRefusal;\n\ + \n\ + mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + }\n\ + \n\ + impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 1, "{:?}", verdict.offenders); + assert!(verdict.offenders.is_empty(), "{:?}", verdict.offenders); + } + + /// A different prefix is not normalized into the immediate child. Doing so + /// would be partial name resolution and could lift a body from another + /// scope merely because its terminal spelling matched. + #[test] + fn a_nonlocal_prefix_does_not_lift_an_immediate_child() { + let verdict = coupled_body_verdict(&source( + "pub use super::seat::DemoRefusal;\n\ + \n\ + mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + }\n\ + \n\ + impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 0); + assert_eq!(verdict.offenders.len(), 1, "{:?}", verdict.offenders); + } + /// A body a child module declares and nobody re-exports stays in the child's /// own scope, so a family implementation outside it does not resolve against /// it. The lift follows a written re-export and never a name collision. @@ -982,6 +1067,62 @@ mod tests { ); } + /// Planted reversal: identical terminal child names under distinct parent + /// modules are distinct syntactic scopes. A family in the first child may + /// not borrow the body declared in the second child merely because both + /// chains end in `seat`. + #[test] + fn identical_nested_terminal_names_do_not_cross_couple() { + let verdict = coupled_body_verdict(&source( + "mod first {\n\ + \x20 mod seat {\n\ + \x20 impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + \x20 }\n\ + \x20 }\n\ + }\n\ + mod second {\n\ + \x20 mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + \x20 }\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 0, "{:?}", verdict.offenders); + assert_eq!(verdict.offenders.len(), 1, "{:?}", verdict.offenders); + assert!( + verdict + .offenders + .first() + .is_some_and(|offence| offence.contains("no `pub struct DemoRefusal`")), + "{:?}", + verdict.offenders + ); + } + + /// Positive control: a body and its family nested together through more + /// than one inline-module segment still share one full syntactic scope. + #[test] + fn a_nested_family_and_body_share_their_full_scope() { + let verdict = coupled_body_verdict(&source( + "mod parent {\n\ + \x20 mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + \x20 impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + \x20 }\n\ + \x20 }\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 1, "{:?}", verdict.offenders); + assert!(verdict.offenders.is_empty(), "{:?}", verdict.offenders); + } + /// A source this reader cannot parse is a hole in the population, and it is /// reported as one. Silently reading it as "no families here" is the exact /// failure the derived denominator exists to prevent. diff --git a/xtask/src/checks/dependency.rs b/xtask/src/checks/dependency.rs index 89e10d0..9b00c9f 100644 --- a/xtask/src/checks/dependency.rs +++ b/xtask/src/checks/dependency.rs @@ -15,7 +15,9 @@ //! **What cargo NORMALIZES those manifests to** is a different question, and the //! documents cannot answer it: an edge can arrive through workspace //! inheritance, and a package identity is something only cargo settles. -//! `cargo metadata` is asked directly. +//! `cargo metadata` is asked directly. Its answer is a role-distinct live +//! observation taken beside the committed file projection; Cargo's executable, +//! environment, configuration, and cache do not become committed facts. //! //! The subject on that side is `packages[].dependencies` — each package's //! dependency DECLARATIONS, normalized — and not `resolve.nodes[].deps`, which @@ -78,7 +80,10 @@ const JUDGE_PACKAGE: &str = "threadpak-testpak"; /// second, which is why the second is not optional and why an unavailable /// reading refuses rather than passes. pub(crate) fn check_no_core_tooling_edge(snapshot: &RepositorySnapshot) -> Result<(), String> { - let census = snapshot.cargo().census(); + let census = snapshot + .cargo() + .census() + .required("the committed manifest dependency census")?; let mut reported: Vec = census .of(MANIFEST_FILE) .into_iter() @@ -93,7 +98,7 @@ pub(crate) fn check_no_core_tooling_edge(snapshot: &RepositorySnapshot) -> Resul .map(|violation| format!("services reach their expansion surface: {violation}")), ); let normalized = snapshot - .cargo() + .cargo_observation() .normalized() .required("what cargo normalized this workspace's manifests to")?; reported.extend(normalized_offences( @@ -232,6 +237,7 @@ fn points_into(path: &str, directory: &str) -> bool { #[cfg(test)] mod tests { use super::{check_no_core_tooling_edge, judge_core_declaration, judge_services_declaration}; + use crate::checks::scratch::Scratch; use crate::repository::cargo::dependency_declarations; use crate::repository::snapshot::repository_snapshot; use crate::repository::types::CanonicalPath; @@ -441,13 +447,84 @@ mod tests { Ok(()) } + /// Aggregate reversal: a malformed committed manifest makes the derived + /// census unreadable. It cannot silently contribute an empty edge set. + #[test] + fn an_unreadable_committed_manifest_refuses_the_dependency_census() -> Result<(), String> { + let scratch = Scratch::named("dependency-unreadable-manifest")?; + scratch.write("Cargo.toml", "[package\nname = \"broken\"\n")?; + let snapshot = scratch.read()?; + let found = check_no_core_tooling_edge(&snapshot); + assert!( + found.is_err_and( + |refusal| refusal.contains("committed manifest dependency census") + && refusal.contains("Cargo.toml") + && refusal.contains("could not be read") + ), + "an unreadable manifest became an empty census contribution" + ); + Ok(()) + } + + /// Aggregate reversal for the role-distinct live Cargo leg. The committed + /// declaration is written at the innocent local key `helpers` and inherits + /// both package identity and path from the workspace pool, so the local + /// manifest census alone reports no offence. Cargo normalization resolves + /// that inherited declaration to the forbidden services package. + #[test] + fn cargo_normalization_catches_an_inherited_renamed_edge() -> Result<(), String> { + let scratch = Scratch::named("dependency-normalized-inheritance")?; + scratch.write( + "Cargo.toml", + "[package]\nname = \"threadpak\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\ + [workspace]\nmembers = [\"macros/macroc\"]\nresolver = \"3\"\n\n\ + [workspace.dependencies]\nhelpers = { package = \"threadpak-macroc\", path = \"macros/macroc\" }\n\n\ + [dependencies]\nhelpers.workspace = true\n", + )?; + scratch.write("src/lib.rs", "//! fixture core\n")?; + scratch.write( + "macros/macroc/Cargo.toml", + "[package]\nname = \"threadpak-macroc\"\nversion = \"0.0.0\"\nedition = \"2024\"\n", + )?; + scratch.write("macros/macroc/src/lib.rs", "//! fixture services\n")?; + scratch.write( + "Cargo.lock", + "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\nversion = 4\n\n\ + [[package]]\nname = \"threadpak\"\nversion = \"0.0.0\"\ndependencies = [\n \"threadpak-macroc\",\n]\n\n\ + [[package]]\nname = \"threadpak-macroc\"\nversion = \"0.0.0\"\n", + )?; + let snapshot = scratch.read()?; + let census = snapshot + .cargo() + .census() + .required("the committed manifest dependency census")?; + let declared_offences: Vec<_> = census + .of("Cargo.toml") + .into_iter() + .filter_map(judge_core_declaration) + .collect(); + assert!( + declared_offences.is_empty(), + "the hostile did not isolate Cargo normalization: {declared_offences:?}" + ); + + let found = check_no_core_tooling_edge(&snapshot); + assert!( + found.is_err_and( + |refusal| refusal.contains("as cargo normalized the manifests") + && refusal.contains("threadpak-macroc") + ), + "the live Cargo observation did not catch the inherited renamed edge" + ); + Ok(()) + } + /// The real repository holds, on BOTH authorities at once. /// - /// This is the only test here that reaches the resolved half, because the - /// resolved half is cargo's answer about a real workspace and cannot be - /// written as a fixture. It is also where the law's refusal-on-unknown is - /// exercised: a run that could not ask cargo fails here rather than - /// reporting an absence nobody established. + /// This reaches Cargo's live normalization over the actual workspace, while + /// the aggregate hostile above proves that leg can independently activate. + /// It also exercises refusal-on-unknown: a run that could not ask Cargo + /// fails here rather than reporting an absence nobody established. #[test] fn the_real_workspace_carries_no_prohibited_edge() -> Result<(), String> { let found = check_no_core_tooling_edge(repository_snapshot()?); diff --git a/xtask/src/checks/hygiene.rs b/xtask/src/checks/hygiene.rs index a31afb6..92f699b 100644 --- a/xtask/src/checks/hygiene.rs +++ b/xtask/src/checks/hygiene.rs @@ -308,11 +308,11 @@ mod tests { /// here, and this doc line is where that is admitted rather than implied. #[test] fn a_crlf_file_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("lf-only"); - scratch.write("clean.md", "one line\nanother\n"); + let scratch = Scratch::named("lf-only")?; + scratch.write("clean.md", "one line\nanother\n")?; assert!(check_lf_and_no_symlinks(&scratch.read()?).is_ok()); - scratch.write("drifted.md", "one line\r\nanother\r\n"); + scratch.write("drifted.md", "one line\r\nanother\r\n")?; let found = check_lf_and_no_symlinks(&scratch.read()?); assert!(found.is_err_and(|reason| reason.contains("CRLF") && reason.contains("drifted"))); Ok(()) @@ -321,12 +321,12 @@ mod tests { /// Planted reversal: a Python file anywhere in the tree. #[test] fn a_python_file_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("no-python"); - scratch.write("tool.rs", "fn main() {}\n"); - scratch.write("notes/readme.md", "prose\n"); + let scratch = Scratch::named("no-python")?; + scratch.write("tool.rs", "fn main() {}\n")?; + scratch.write("notes/readme.md", "prose\n")?; assert!(check_no_python(&scratch.read()?).is_ok()); - scratch.write("notes/helper.py", "the file's presence is the offence\n"); + scratch.write("notes/helper.py", "the file's presence is the offence\n")?; let found = check_no_python(&scratch.read()?); assert!(found.is_err_and(|reason| reason.contains("helper.py"))); Ok(()) @@ -337,11 +337,11 @@ mod tests { /// covers, so no tree is scanned in name only. #[test] fn an_underscore_field_carrying_data_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("underscore-fields"); + let scratch = Scratch::named("underscore-fields")?; let lawful = "use core::marker::PhantomData;\n\ pub struct Demo {\n _law: PhantomData<*const ()>,\n}\n"; for tree in SCANNED_TREES { - scratch.write(&format!("{tree}/lawful.rs"), lawful); + scratch.write(&format!("{tree}/lawful.rs"), lawful)?; } assert!(check_underscore_fields_are_phantom(&scratch.read()?).is_ok()); @@ -349,14 +349,14 @@ mod tests { scratch.write( &format!("{tree}/smuggled.rs"), "pub struct Demo {\n _hidden: u64,\n}\n", - ); + )?; let found = check_underscore_fields_are_phantom(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("smuggled.rs") && reason.contains("underscore field without PhantomData")), "{tree} tree is not scanned" ); - scratch.remove(&format!("{tree}/smuggled.rs")); + scratch.remove(&format!("{tree}/smuggled.rs"))?; } Ok(()) } diff --git a/xtask/src/checks/obligations.rs b/xtask/src/checks/obligations.rs index 5a13926..0eab7f3 100644 --- a/xtask/src/checks/obligations.rs +++ b/xtask/src/checks/obligations.rs @@ -1952,8 +1952,8 @@ mod tests { /// missing one gets, since the leg refuses on the first. #[test] fn a_missing_tooling_ledger_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("tooling-ledger-missing"); - scratch.write("README.md", "# a tree with no tooling ledger\n"); + let scratch = Scratch::named("tooling-ledger-missing")?; + scratch.write("README.md", "# a tree with no tooling ledger\n")?; let found = tooling_rows(&scratch.read()?); assert!(found.is_err(), "{found:?}"); assert!( diff --git a/xtask/src/checks/parity.rs b/xtask/src/checks/parity.rs index 5b2d149..d437c43 100644 --- a/xtask/src/checks/parity.rs +++ b/xtask/src/checks/parity.rs @@ -36,12 +36,12 @@ mod tests { /// edited alone is exactly how a working law stops being one law. #[test] fn a_drifted_working_law_pair_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("agents-parity"); - scratch.write("AGENTS.md", "the working law\n"); - scratch.write("CLAUDE.md", "the working law\n"); + let scratch = Scratch::named("agents-parity")?; + scratch.write("AGENTS.md", "the working law\n")?; + scratch.write("CLAUDE.md", "the working law\n")?; assert!(check_agents_claude_parity(&scratch.read()?).is_ok()); - scratch.write("CLAUDE.md", "the working law, edited on one side only\n"); + scratch.write("CLAUDE.md", "the working law, edited on one side only\n")?; let found = check_agents_claude_parity(&scratch.read()?); assert!(found.is_err_and(|reason| reason.contains("differ"))); Ok(()) @@ -52,8 +52,8 @@ mod tests { /// with empty bytes would have found two empty files identical. #[test] fn a_missing_half_of_the_working_law_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("agents-parity-missing"); - scratch.write("AGENTS.md", "the working law\n"); + let scratch = Scratch::named("agents-parity-missing")?; + scratch.write("AGENTS.md", "the working law\n")?; let found = check_agents_claude_parity(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("CLAUDE.md") && reason.contains("not there")), diff --git a/xtask/src/checks/placement.rs b/xtask/src/checks/placement.rs index 52bc14d..df1e9a2 100644 --- a/xtask/src/checks/placement.rs +++ b/xtask/src/checks/placement.rs @@ -713,7 +713,7 @@ fn separator_follows(trees: &[TokenTree], index: usize) -> Option<&TokenTree> { } /// The segment heads one crate-root path reaches: the single name of a plain -/// path, or every head inside a grouped use, at any nesting. +/// path, or every path rooted directly inside a grouped use. fn referenced_heads(tail: &TokenTree) -> Vec { match *tail { TokenTree::Ident(ref named) => vec![named.to_string()], @@ -724,7 +724,11 @@ fn referenced_heads(tail: &TokenTree) -> Vec { } } -/// Every head inside one grouped use, at any nesting. +/// Every root head inside one grouped use. +/// +/// A nested group continues the path whose head preceded it. Its members are +/// below that root and are not sibling modules of it, so this bounded token +/// grammar deliberately does not descend into nested brace groups. fn grouped_heads(tokens: TokenStream) -> Vec { let mut heads = Vec::new(); let mut at_head = true; @@ -738,7 +742,6 @@ fn grouped_heads(tokens: TokenStream) -> Vec { } } TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => { - heads.extend(grouped_heads(group.stream())); at_head = false; } TokenTree::Group(_) | TokenTree::Literal(_) | TokenTree::Punct(_) => (), @@ -1002,6 +1005,16 @@ mod tests { Ok(()) } + /// The heads of a grouped use are the paths rooted directly in the group. + /// Nested members remain beneath their root and do not become crate-root + /// module edges of their own. + #[test] + fn a_nested_grouped_use_reports_only_its_root_heads() -> Result<(), String> { + let references = references_of("use crate::{a::{b, c}, d};\n", ModuleLayout::Flat)?; + assert_eq!(references, vec![String::from("a"), String::from("d")]); + Ok(()) + } + /// The positive control: a clean set passes. Backward references, repeated /// references, a module naming itself, and a longer identifier merely /// ENDING in `crate` are all lawful, so the check reports something real @@ -1061,26 +1074,26 @@ mod tests { /// crate drift apart, and the third is the one no file listing would catch. #[test] fn a_band_map_that_drifts_from_lib_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("band-map"); + let scratch = Scratch::named("band-map")?; let ordered = "#[path = \"00_refusal/mod.rs\"]\npub mod refusal;\n\ #[path = \"01_logic/mod.rs\"]\npub mod logic;\n"; for band in ["00_refusal", "01_logic"] { for file in ["README.md", "mod.rs", "types.rs"] { - scratch.write(&format!("src/{band}/{file}"), "the home's content\n"); + scratch.write(&format!("src/{band}/{file}"), "the home's content\n")?; } } - scratch.write("src/lib.rs", ordered); + scratch.write("src/lib.rs", ordered)?; assert!(check_band_map(&scratch.read()?).is_ok()); - scratch.remove("src/01_logic/types.rs"); + scratch.remove("src/01_logic/types.rs")?; let incomplete = check_band_map(&scratch.read()?); assert!(incomplete.is_err_and(|reason| reason.contains("01_logic missing types.rs"))); - scratch.write("src/01_logic/types.rs", "the home's content\n"); + scratch.write("src/01_logic/types.rs", "the home's content\n")?; scratch.write( "src/lib.rs", "#[path = \"00_refusal/mod.rs\"]\npub mod refusal;\n", - ); + )?; let undeclared = check_band_map(&scratch.read()?); assert!(undeclared.is_err_and(|reason| reason.contains("does not declare 01_logic"))); @@ -1088,7 +1101,7 @@ mod tests { "src/lib.rs", "#[path = \"01_logic/mod.rs\"]\npub mod logic;\n\ #[path = \"00_refusal/mod.rs\"]\npub mod refusal;\n", - ); + )?; let reordered = check_band_map(&scratch.read()?); assert!(reordered.is_err_and(|reason| reason.contains("out of band order"))); Ok(()) @@ -1104,14 +1117,14 @@ mod tests { /// whitespace. #[test] fn a_band_declaration_is_read_however_it_is_spaced() -> Result<(), String> { - let scratch = Scratch::named("band-map-spacing"); + let scratch = Scratch::named("band-map-spacing")?; for file in ["README.md", "mod.rs", "types.rs"] { - scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n"); + scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n")?; } scratch.write( "src/lib.rs", "#[path=\"00_refusal/mod.rs\"]\npub mod refusal;\n", - ); + )?; let found = check_band_map(&scratch.read()?); assert!(found.is_ok(), "{found:?}"); Ok(()) @@ -1127,16 +1140,16 @@ mod tests { /// in it. #[test] fn an_entry_under_src_that_is_no_home_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("src-population"); + let scratch = Scratch::named("src-population")?; scratch.write( "src/lib.rs", "#[path = \"00_refusal/mod.rs\"]\npub mod refusal;\n", - ); + )?; for file in HOME_FILES { - scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n"); + scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n")?; } for reserved in RESERVED_ROOT_FILES { - scratch.write(&format!("src/{reserved}"), "the root's own file\n"); + scratch.write(&format!("src/{reserved}"), "the root's own file\n")?; } assert!(check_band_map(&scratch.read()?).is_ok()); @@ -1146,13 +1159,13 @@ mod tests { ("src/05_/mod.rs", "band number and no name"), ("src/notes.md", "sits directly in"), ] { - scratch.write(planted, "an entry the population never classified\n"); + scratch.write(planted, "an entry the population never classified\n")?; let found = check_band_map(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains(said)), "{planted} left the population in silence" ); - scratch.remove(planted); + scratch.remove(planted)?; assert!(check_band_map(&scratch.read()?).is_ok(), "{planted}"); } Ok(()) @@ -1166,14 +1179,14 @@ mod tests { /// stood in all of them. #[test] fn a_conditionally_declared_band_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("band-map-condition"); + let scratch = Scratch::named("band-map-condition")?; for file in HOME_FILES { - scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n"); + scratch.write(&format!("src/00_refusal/{file}"), "the home's content\n")?; } scratch.write( "src/lib.rs", "#[cfg(unix)]\n#[path = \"00_refusal/mod.rs\"]\npub mod refusal;\n", - ); + )?; let found = check_band_map(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("under a build condition")), @@ -1190,16 +1203,16 @@ mod tests { /// the declarations never had a word to say about it. #[test] fn a_services_source_no_declaration_accounts_for_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("tooling-population"); + let scratch = Scratch::named("tooling-population")?; scratch.write( "macros/macroc/src/lib.rs", "pub mod plane;\n\n#[cfg(test)]\nmod laws;\n", - ); - scratch.write("macros/macroc/src/plane.rs", "//! no edges at all\n"); - scratch.write("macros/macroc/src/laws.rs", "//! the proof surface\n"); + )?; + scratch.write("macros/macroc/src/plane.rs", "//! no edges at all\n")?; + scratch.write("macros/macroc/src/laws.rs", "//! the proof surface\n")?; assert!(check_tooling_module_order(&scratch.read()?).is_ok()); - scratch.write("macros/macroc/src/orphan.rs", "//! nobody declares this\n"); + scratch.write("macros/macroc/src/orphan.rs", "//! nobody declares this\n")?; let found = check_tooling_module_order(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("orphan.rs")), @@ -1216,19 +1229,19 @@ mod tests { /// the check went on reporting about the module that owns it. #[test] fn a_module_written_as_both_a_file_and_a_directory_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("tooling-layout"); + let scratch = Scratch::named("tooling-layout")?; scratch.write( "macros/macroc/src/lib.rs", "pub mod plane;\npub mod token;\n", - ); - scratch.write("macros/macroc/src/plane.rs", "//! no edges at all\n"); - scratch.write("macros/macroc/src/token.rs", "use crate::plane::Own;\n"); + )?; + scratch.write("macros/macroc/src/plane.rs", "//! no edges at all\n")?; + scratch.write("macros/macroc/src/token.rs", "use crate::plane::Own;\n")?; assert!(check_tooling_module_order(&scratch.read()?).is_ok()); scratch.write( "macros/macroc/src/plane/inner.rs", "use crate::token::Reaching;\n", - ); + )?; let found = check_tooling_module_order(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("written twice")), diff --git a/xtask/src/checks/positivity.rs b/xtask/src/checks/positivity.rs index d6495d3..006a50d 100644 --- a/xtask/src/checks/positivity.rs +++ b/xtask/src/checks/positivity.rs @@ -13,10 +13,11 @@ //! established by VALUES instead: `LimitWitness` carries the selection and //! `PositiveLimitWitness` carries the promise that it admits an item. //! -//! `PositiveLimitWitness::inhabited` is bounded on `EvidenceSelectedLimit`, so a -//! family that never declared the runtime ladder has no road to a runtime -//! capacity at all, and the compile-fail fixtures name that refusal from outside -//! the crate. That half is `rustc`'s and this law does not restate it. +//! `LimitWitness` itself is bounded on `EvidenceSelectedLimit`, and every +//! consumer inherits or states the same authority, so a family that never +//! declared the runtime ladder has no road to a runtime capacity at all. The +//! compile-fail fixtures name that refusal from outside the crate. That half is +//! `rustc`'s and this law does not restate it. //! //! What `rustc` cannot say is the UNIVERSAL sentence: that every family which //! needs the runtime ladder is on it. Rust cannot enumerate the types @@ -337,7 +338,8 @@ fn read_items(path: &str, items: &[syn::Item], reading: &mut Reading) { /// Reads one implementation: which ladder it declares for which family, and the /// seats its own members declare. fn read_implementation(path: &str, declared: &syn::ItemImpl, reading: &mut Reading) { - if let Some((contract, _)) = &declared.trait_ + if declared.modifiers.polarity.is_none() + && let Some((contract, _)) = &declared.trait_ && let Some(contract) = last_segment(contract) && let Some(family) = head_of(&declared.self_ty) { @@ -653,6 +655,26 @@ mod tests { assert!(after.offenders.is_empty(), "{:?}", after.offenders); } + /// A negative impl states that the family is not on the named ladder. Syn + /// accepts this unstable spelling even on the pinned stable toolchain; the + /// reader must not invert its polarity and count it as a positive witness. + #[test] + fn a_negative_runtime_impl_is_not_a_positive_witness() { + let verdict = positivity_verdict(&source( + "pub struct DemoIssueLimit;\n\ + impl Limit for DemoIssueLimit {\n\ + \x20 type Authority = UnstatedMagnitude;\n\ + }\n\ + impl !EvidenceSelectedLimit for DemoIssueLimit {}\n\ + pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.witnessed, 0); + assert_eq!(verdict.offenders.len(), 1, "{:?}", verdict.offenders); + } + /// A family with a compile-time magnitude is outside this population /// entirely: the declared road is open to it, `PositiveLimit` proves its /// positivity before the program runs, and the runtime ladder is not the diff --git a/xtask/src/checks/scratch.rs b/xtask/src/checks/scratch.rs index c970e4c..97d1fc7 100644 --- a/xtask/src/checks/scratch.rs +++ b/xtask/src/checks/scratch.rs @@ -6,14 +6,16 @@ //! the same snapshot builder the real run uses — a law proven against a reading //! built by different machinery would be a law proven against a different model. //! Nothing is written inside the repository — the laws that guard the tree are -//! never proven by dirtying the tree — and each root is removed when its fixture -//! drops. +//! never proven by dirtying the tree. Fixture planting and deliberate removal +//! carry every filesystem refusal; drop makes a best-effort cleanup only after +//! the fixture can no longer affect a verdict. //! //! This module exists only under `cfg(test)`: it is fixture machinery shared by //! several law families, and it ships in no binary. use std::fs; use std::path::PathBuf; +use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicUsize, Ordering}; use crate::repository::snapshot::RepositorySnapshot; @@ -28,46 +30,136 @@ impl Scratch { /// A fresh scratch root, named for the reversal that built it. The /// process id and a run counter keep two fixtures — and two concurrent /// runs — from sharing one root. - pub(crate) fn named(name: &str) -> Self { + pub(crate) fn named(name: &str) -> Result { static NEXT: AtomicUsize = AtomicUsize::new(0); let ordinal = NEXT.fetch_add(1, Ordering::Relaxed); let root = std::env::temp_dir().join(format!( "threadpak-xtask-{}-{ordinal}-{name}", std::process::id() )); - let _cleared = fs::remove_dir_all(&root); - let _made = fs::create_dir_all(&root); - Self { root } + match fs::remove_dir_all(&root) { + Ok(()) => (), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => (), + Err(error) => { + return Err(format!( + "could not clear scratch root {}: {error}", + root.display() + )); + } + } + fs::create_dir_all(&root).map_err(|error| { + format!("could not create scratch root {}: {error}", root.display()) + })?; + Ok(Self { root }) } /// Plants one file at a root-relative path, creating its parents. - pub(crate) fn write(&self, relative: &str, contents: &str) { + pub(crate) fn write(&self, relative: &str, contents: &str) -> Result<(), String> { let path = self.root.join(relative); if let Some(parent) = path.parent() { - let _made = fs::create_dir_all(parent); + fs::create_dir_all(parent) + .map_err(|error| format!("could not create {}: {error}", parent.display()))?; } - let _written = fs::write(&path, contents); + fs::write(&path, contents) + .map_err(|error| format!("could not write scratch file {}: {error}", path.display())) } /// Removes one planted file, so a reversal can plant an absence. - pub(crate) fn remove(&self, relative: &str) { - let _removed = fs::remove_file(self.root.join(relative)); + pub(crate) fn remove(&self, relative: &str) -> Result<(), String> { + let path = self.root.join(relative); + fs::remove_file(&path) + .map_err(|error| format!("could not remove scratch file {}: {error}", path.display())) } /// The reading of the fixture tree, taken by the builder the real run uses. /// - /// A fixture root is not a workspace and not a checkout, so what cargo - /// resolved and what git says are DECLARED absences here rather than empty - /// values. A law that needs either is refused against a fixture, which is - /// the honest answer and is why the laws that read a fixture are the laws - /// that need neither. + /// The fixture is committed immediately before it is read. Production has + /// no working-tree snapshot road, so a test does not gain one merely for + /// convenience: the same Git-owned population and immutable-blob builder + /// reads both real and planted trees. pub(crate) fn read(&self) -> Result { + self.commit_current_tree()?; RepositorySnapshot::read(&self.root) } + + /// Makes the fixture's current files one clean committed tree. + fn commit_current_tree(&self) -> Result<(), String> { + if !self.root.join(".git").exists() { + self.git(&["init", "--quiet"])?; + self.git(&["config", "user.name", "ThreadPak fixture"])?; + self.git(&["config", "user.email", "fixture@threadpak.invalid"])?; + self.git(&["config", "core.autocrlf", "false"])?; + } + self.git(&["add", "-A"])?; + self.git(&[ + "commit", + "--quiet", + "--allow-empty", + "--message", + "planted fixture state", + ])?; + Ok(()) + } + + /// Runs one Git operation against this fixture and carries its refusal. + fn git(&self, arguments: &[&str]) -> Result<(), String> { + let output = Command::new("git") + .current_dir(&self.root) + .env("GIT_NO_REPLACE_OBJECTS", "1") + .args(arguments) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("fixture git {}: {error}", arguments.join(" ")))?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "fixture git {} refused: {}", + arguments.join(" "), + String::from_utf8_lossy(&output.stderr).trim() + )) + } + } } impl Drop for Scratch { fn drop(&mut self) { + // Destructors cannot return a refusal. Every semantic planting/removal + // road above is fallible; this is only best-effort temporary cleanup + // after the fixture can no longer affect a verdict. let _removed = fs::remove_dir_all(&self.root); } } + +#[cfg(test)] +mod tests { + use super::Scratch; + + /// A failed removal is an explicit fixture failure, not a successful + /// absence plant that lets the challenged law run against the wrong tree. + #[test] + fn removing_a_path_that_was_never_planted_refuses() -> Result<(), String> { + let scratch = Scratch::named("missing-removal")?; + let found = scratch.remove("never-there.txt"); + assert!( + found.is_err_and(|refusal| refusal.contains("never-there.txt")), + "a failed scratch removal was discarded" + ); + Ok(()) + } + + /// A failed write is likewise explicit. Planting a file where the next + /// write needs a directory makes the failure deterministic on every host. + #[test] + fn a_write_whose_parent_cannot_be_created_refuses() -> Result<(), String> { + let scratch = Scratch::named("blocked-parent")?; + scratch.write("blocked", "an ordinary file\n")?; + let found = scratch.write("blocked/child.txt", "cannot be planted\n"); + assert!( + found.is_err_and(|refusal| refusal.contains("blocked")), + "a failed scratch write was discarded" + ); + Ok(()) + } +} diff --git a/xtask/src/checks/supply_chain.rs b/xtask/src/checks/supply_chain.rs index 29cfea3..0753d65 100644 --- a/xtask/src/checks/supply_chain.rs +++ b/xtask/src/checks/supply_chain.rs @@ -248,12 +248,12 @@ mod tests { const WORKFLOW_FIXTURE: &str = "name: dependencies\n"; /// One scratch root carrying a whole, lawful gate. - fn planted(name: &str) -> Scratch { - let scratch = Scratch::named(name); - scratch.write(&GATE_WORKFLOW.join("/"), WORKFLOW_FIXTURE); - scratch.write(RULE_SET, RULE_SET_FIXTURE); - scratch.write(PLANTED_REVERSAL, REVERSAL_FIXTURE); - scratch + fn planted(name: &str) -> Result { + let scratch = Scratch::named(name)?; + scratch.write(&GATE_WORKFLOW.join("/"), WORKFLOW_FIXTURE)?; + scratch.write(RULE_SET, RULE_SET_FIXTURE)?; + scratch.write(PLANTED_REVERSAL, REVERSAL_FIXTURE)?; + Ok(scratch) } /// The positive control: a gate carrying all three of its artifacts, with a @@ -262,7 +262,7 @@ mod tests { /// reversal below and be worthless. #[test] fn present_and_distinct_artifacts_are_lawful() -> Result<(), String> { - let scratch = planted("gate-whole"); + let scratch = planted("gate-whole")?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!(found.is_ok(), "{found:?}"); Ok(()) @@ -273,8 +273,8 @@ mod tests { /// be watched is gone, and nothing else in the tree notices. #[test] fn a_deleted_reversal_is_a_violation() -> Result<(), String> { - let scratch = planted("gate-reversal-deleted"); - scratch.remove(PLANTED_REVERSAL); + let scratch = planted("gate-reversal-deleted")?; + scratch.remove(PLANTED_REVERSAL)?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_err_and( @@ -289,8 +289,8 @@ mod tests { /// configurations survive and no committed file names a run that reads them. #[test] fn a_deleted_workflow_is_a_violation() -> Result<(), String> { - let scratch = planted("gate-workflow-deleted"); - scratch.remove(&GATE_WORKFLOW.join("/")); + let scratch = planted("gate-workflow-deleted")?; + scratch.remove(&GATE_WORKFLOW.join("/"))?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("hosted seat is not committed")), @@ -303,8 +303,8 @@ mod tests { /// against nothing. #[test] fn a_deleted_rule_set_is_a_violation() -> Result<(), String> { - let scratch = planted("gate-rule-set-deleted"); - scratch.remove(RULE_SET); + let scratch = planted("gate-rule-set-deleted")?; + scratch.remove(RULE_SET)?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains(RULE_SET) && reason.contains("is not there")), @@ -318,8 +318,8 @@ mod tests { /// would report as present. #[test] fn an_emptied_reversal_is_a_violation() -> Result<(), String> { - let scratch = planted("gate-reversal-emptied"); - scratch.write(PLANTED_REVERSAL, "\n \n"); + let scratch = planted("gate-reversal-emptied")?; + scratch.write(PLANTED_REVERSAL, "\n \n")?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("states nothing at all")), @@ -338,8 +338,8 @@ mod tests { /// purpose — has quietly stopped being true of it. #[test] fn a_reversal_that_stopped_departing_is_a_violation() -> Result<(), String> { - let scratch = planted("gate-reversal-synchronized"); - scratch.write(PLANTED_REVERSAL, RULE_SET_FIXTURE); + let scratch = planted("gate-reversal-synchronized")?; + scratch.write(PLANTED_REVERSAL, RULE_SET_FIXTURE)?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("byte-for-byte")), @@ -361,11 +361,11 @@ mod tests { /// back. #[test] fn a_workflow_that_invokes_nothing_still_passes() -> Result<(), String> { - let scratch = planted("gate-workflow-invokes-nothing"); + let scratch = planted("gate-workflow-invokes-nothing")?; scratch.write( &GATE_WORKFLOW.join("/"), "name: dependencies\non:\n pull_request:\njobs: {}\n", - ); + )?; let found = check_dependency_gate_artifacts(&scratch.read()?); assert!( found.is_ok(), diff --git a/xtask/src/checks/toolchain.rs b/xtask/src/checks/toolchain.rs index c9c6a8f..385c0b9 100644 --- a/xtask/src/checks/toolchain.rs +++ b/xtask/src/checks/toolchain.rs @@ -238,27 +238,27 @@ mod tests { /// for, where the pin said 1.97.1 while two files said 1.97. #[test] fn a_floor_that_drifts_from_the_pin_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("toolchain-pin"); - scratch.write("README.md", FIXTURE_README); - scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.97.1\"\n"); + let scratch = Scratch::named("toolchain-pin")?; + scratch.write("README.md", FIXTURE_README)?; + scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.97.1\"\n")?; scratch.write( "Cargo.toml", "[workspace.package]\nrust-version = \"1.97.1\"\n", - ); - scratch.write("clippy.toml", "msrv = \"1.97.1\"\n"); + )?; + scratch.write("clippy.toml", "msrv = \"1.97.1\"\n")?; assert!(check_toolchain_pin(&scratch.read()?).is_ok()); - scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.98.0\"\n"); + scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.98.0\"\n")?; let drifted_readme = check_toolchain_pin(&scratch.read()?); assert!(drifted_readme.is_err_and( |reason| reason.contains("1.98.0") && reason.contains("README declares 1.97.1") )); - scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.97.1\"\n"); + scratch.write("rust-toolchain.toml", "[toolchain]\nchannel = \"1.97.1\"\n")?; scratch.write( "Cargo.toml", "[workspace.package]\nrust-version = \"1.97\"\n", - ); + )?; let drifted_floor = check_toolchain_pin(&scratch.read()?); assert!( drifted_floor.is_err_and(|reason| reason.contains("Cargo.toml rust-version is 1.97")) @@ -267,8 +267,8 @@ mod tests { scratch.write( "Cargo.toml", "[workspace.package]\nrust-version = \"1.97.1\"\n", - ); - scratch.write("clippy.toml", "msrv = \"1.97\"\n"); + )?; + scratch.write("clippy.toml", "msrv = \"1.97\"\n")?; let drifted_suggestions = check_toolchain_pin(&scratch.read()?); assert!( drifted_suggestions.is_err_and(|reason| reason.contains("clippy.toml msrv is 1.97")) @@ -281,19 +281,19 @@ mod tests { /// from the manifest while the README still lists it. #[test] fn a_member_set_that_drifts_from_the_readme_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("workspace-members"); - scratch.write("README.md", FIXTURE_README); - scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n"); + let scratch = Scratch::named("workspace-members")?; + scratch.write("README.md", FIXTURE_README)?; + scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n")?; assert!(check_workspace_members(&scratch.read()?).is_ok()); scratch.write( "Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\", \"three\"]\n", - ); + )?; let added = check_workspace_members(&scratch.read()?); assert!(added.is_err_and(|reason| reason.contains("three"))); - scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\"]\n"); + scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\"]\n")?; let removed = check_workspace_members(&scratch.read()?); assert!(removed.is_err_and(|reason| reason.contains("two"))); Ok(()) @@ -305,23 +305,23 @@ mod tests { /// check names them apart. #[test] fn a_member_outside_the_lint_wall_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("lint-wall"); + let scratch = Scratch::named("lint-wall")?; let inheriting = "[package]\nname = \"member\"\n\n[lints]\nworkspace = true\n"; scratch.write( "Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n\n[workspace.lints.rust]\n\ warnings = { level = \"deny\", priority = -1 }\n", - ); - scratch.write("one/Cargo.toml", inheriting); - scratch.write("two/Cargo.toml", inheriting); + )?; + scratch.write("one/Cargo.toml", inheriting)?; + scratch.write("two/Cargo.toml", inheriting)?; assert!(check_lint_wall(&scratch.read()?).is_ok()); - scratch.write("two/Cargo.toml", "[package]\nname = \"member\"\n"); + scratch.write("two/Cargo.toml", "[package]\nname = \"member\"\n")?; let escaped = check_lint_wall(&scratch.read()?); assert!(escaped.is_err_and(|reason| reason.contains("two") && !reason.contains("\"one\""))); - scratch.write("two/Cargo.toml", inheriting); - scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n"); + scratch.write("two/Cargo.toml", inheriting)?; + scratch.write("Cargo.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n")?; let wall_free = check_lint_wall(&scratch.read()?); assert!(wall_free.is_err_and(|reason| reason.contains("no [workspace.lints.rust] wall"))); Ok(()) @@ -336,20 +336,20 @@ mod tests { /// over it because the array it read still listed six members that inherit. #[test] fn a_root_package_outside_the_lint_wall_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("lint-wall-root"); + let scratch = Scratch::named("lint-wall-root")?; let inheriting = "[package]\nname = \"member\"\n\n[lints]\nworkspace = true\n"; let root = "[package]\nname = \"machine\"\n\n[lints]\nworkspace = true\n\n\ [workspace]\nmembers = [\"one\"]\n\n[workspace.lints.rust]\n\ warnings = { level = \"deny\", priority = -1 }\n"; - scratch.write("Cargo.toml", root); - scratch.write("one/Cargo.toml", inheriting); + scratch.write("Cargo.toml", root)?; + scratch.write("one/Cargo.toml", inheriting)?; assert!(check_lint_wall(&scratch.read()?).is_ok()); scratch.write( "Cargo.toml", "[package]\nname = \"machine\"\n\n[workspace]\nmembers = [\"one\"]\n\n\ [workspace.lints.rust]\nwarnings = { level = \"deny\", priority = -1 }\n", - ); + )?; let stripped = check_lint_wall(&scratch.read()?); assert!( stripped.is_err_and(|reason| reason.contains("Cargo.toml does not inherit") @@ -369,20 +369,20 @@ mod tests { /// declaration rather than off a list kept in this file. #[test] fn a_package_the_array_never_lists_is_still_in_the_population() -> Result<(), String> { - let scratch = Scratch::named("lint-wall-unlisted"); + let scratch = Scratch::named("lint-wall-unlisted")?; scratch.write( "Cargo.toml", "[workspace]\nmembers = [\"one\"]\nexclude = [\"outside\"]\n\n\ [workspace.lints.rust]\nwarnings = { level = \"deny\", priority = -1 }\n", - ); + )?; scratch.write( "one/Cargo.toml", "[package]\nname = \"member\"\n\n[lints]\nworkspace = true\n", - ); - scratch.write("outside/Cargo.toml", "[package]\nname = \"excluded\"\n"); + )?; + scratch.write("outside/Cargo.toml", "[package]\nname = \"excluded\"\n")?; assert!(check_lint_wall(&scratch.read()?).is_ok()); - scratch.write("helpers/Cargo.toml", "[package]\nname = \"unlisted\"\n"); + scratch.write("helpers/Cargo.toml", "[package]\nname = \"unlisted\"\n")?; let unlisted = check_lint_wall(&scratch.read()?); assert!( unlisted.is_err_and(|reason| reason.contains("helpers/Cargo.toml") @@ -396,16 +396,16 @@ mod tests { /// does not resolve, and a derived population would report clean about it. #[test] fn a_member_the_tree_does_not_carry_is_a_violation() -> Result<(), String> { - let scratch = Scratch::named("lint-wall-ghost"); + let scratch = Scratch::named("lint-wall-ghost")?; scratch.write( "Cargo.toml", "[workspace]\nmembers = [\"one\", \"ghost\"]\n\n[workspace.lints.rust]\n\ warnings = { level = \"deny\", priority = -1 }\n", - ); + )?; scratch.write( "one/Cargo.toml", "[package]\nname = \"member\"\n\n[lints]\nworkspace = true\n", - ); + )?; let found = check_lint_wall(&scratch.read()?); assert!(found.is_err_and(|reason| reason.contains("`ghost`"))); Ok(()) @@ -420,16 +420,16 @@ mod tests { /// clean, because the characters it was matching were in the file. #[test] fn inheritance_written_in_a_comment_inherits_nothing() -> Result<(), String> { - let scratch = Scratch::named("lint-wall-comment"); + let scratch = Scratch::named("lint-wall-comment")?; scratch.write( "Cargo.toml", "[workspace]\nmembers = [\"one\"]\n\n[workspace.lints.rust]\n\ warnings = { level = \"deny\", priority = -1 }\n", - ); + )?; scratch.write( "one/Cargo.toml", "[package]\nname = \"member\"\n# [lints]\n# workspace = true\n", - ); + )?; let found = check_lint_wall(&scratch.read()?); assert!( found.is_err_and(|reason| reason.contains("one")), diff --git a/xtask/src/checks/vocabulary.rs b/xtask/src/checks/vocabulary.rs index ecf001c..f9ec02c 100644 --- a/xtask/src/checks/vocabulary.rs +++ b/xtask/src/checks/vocabulary.rs @@ -88,14 +88,16 @@ pub(crate) fn check_no_personal_names(snapshot: &RepositorySnapshot) -> Result<( vec![0x65, 0x61, 0x73, 0x73, 0x61], vec![0x61, 0x79, 0x6f, 0x75, 0x62], ]; - let banned: Vec = banned + let banned: Vec<&str> = banned .iter() - .map(|bytes| String::from_utf8_lossy(bytes).into_owned()) - .collect(); + .map(Vec::as_slice) + .map(std::str::from_utf8) + .collect::>() + .map_err(|error| format!("the personal-name verdict terms are not UTF-8: {error}"))?; let mut offenders = Vec::new(); for (path, fact) in snapshot.files().iter() { let text = fact.text().required(path.as_str())?.to_lowercase(); - if banned.iter().any(|name| text.contains(name.as_str())) { + if banned.iter().any(|name| text.contains(*name)) { offenders.push(path.to_string()); } } @@ -360,19 +362,45 @@ fn spells_banned_word(word: &str) -> Option<&'static str> { }) } -/// Planted reversals for the ban and for its allowlist. +/// Planted reversals for both vocabulary checks and for the retired-word +/// allowlist. /// -/// The scans are pure over text, so every reversal below is a fixture string. -/// Nothing on disk is written, read, or mutated: the law that guards the tree is -/// never proven by dirtying the tree. +/// The retired-word scan is pure over text, so its reversals are fixture +/// strings. The personal-name check owns a repository population, so its +/// reversal is a committed scratch tree read by the production snapshot builder. +/// Nothing is written inside the repository the law guards. #[cfg(test)] mod tests { use super::{ BANNED_VOCABULARY_ALLOWLIST, banned_vocabulary_offences, banned_words_in, - stale_allowlist_offences, + check_no_personal_names, stale_allowlist_offences, }; + use crate::checks::scratch::Scratch; use crate::repository::snapshot::repository_snapshot; + /// The aggregate positive control and planted reversal for the registered + /// personal-name check. Both states are committed before the production + /// snapshot reads them, and the forbidden spelling exists only in the + /// planted file, assembled from numeric bytes at runtime. + #[test] + fn the_personal_name_check_activates_on_its_subject() -> Result<(), String> { + const SUBJECT: &str = "evidence/roles.txt"; + let scratch = Scratch::named("personal-name-vocabulary")?; + scratch.write(SUBJECT, "the realization owner is named only by role\n")?; + check_no_personal_names(&scratch.read()?)?; + + let forbidden = String::from_utf8(vec![0x65, 0x61, 0x73, 0x73, 0x61]) + .map_err(|error| format!("the planted verdict term is not UTF-8: {error}"))?; + scratch.write(SUBJECT, &format!("the role was replaced by {forbidden}\n"))?; + let found = check_no_personal_names(&scratch.read()?); + assert_eq!( + found, + Err(format!("personal name present in: {SUBJECT}")), + "the aggregate check did not name its exact offending path" + ); + Ok(()) + } + /// Planted reversal: the term smuggled into a `camelCase` identifier, where /// no whole-word scan of the text would ever find it. #[test] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 451b06e..ec35478 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -74,36 +74,22 @@ fn main() -> Result<(), Box> { /// The reading comes first and is shared, which is the whole of the typed /// repository model: no law walks the tree, opens a file, or starts a process, /// so two laws cannot be judging two different trees. The run opens by naming -/// what it read — how many files, and WHETHER those files are a committed tree — -/// because a verdict that cannot be attached to a tree is a verdict about -/// nothing in particular, and this campaign has already produced one false green -/// from a restore that preserved a modification time. +/// what committed tree it read, because a verdict that cannot be attached to a +/// tree is a verdict about nothing in particular. /// -/// The opening line no longer names a commit it has not established. It used to: -/// the reading walked the disk and then asked git what `HEAD` was, and printed -/// the two side by side as though one were about the other, so every run on a -/// dirty checkout stated a commit-bound result it had never bound. The binding -/// is established around the read now, and where there is none the sentence says -/// so instead of naming a commit anyway. +/// The opening line names only the immutable Git tree that supplied membership +/// and bytes. Cargo's normalization is a role-distinct live observation taken +/// beside that snapshot; it is not described as another committed projection. fn run_checks(root: &Path) -> Result<(), Box> { let snapshot = RepositorySnapshot::read(root)?; println!( - "read {} files {}", + "read {} committed files at {}; Cargo normalization is a live observation", snapshot.files().count(), - snapshot.binding() + snapshot.committed() ); - // EIGHTEEN, and the number is decided here rather than counted from the - // lines below, because this array's length is the only statement of how many - // repository laws there are and a wrong one is `E0308` rather than a law - // that quietly stopped running. Where it came from, exactly: the two lanes - // this merge joins branched from a roster of SEVENTEEN. One lane made a - // defect class unrepresentable and retired the two readers that had been - // re-implementing name resolution to hunt it — the refusal-mint law and the - // stamped-guard law — replacing both with the one syntax-only law - // `seat-modules-carry-nothing-else`, which is sixteen. Two legs the lanes - // named honestly rather than shipping around land here: - // `alarm-artifacts-are-present-and-distinct` and - // `inhabitant-promising-limits-are-witnessed`. Sixteen and two is eighteen. + // This typed array is the roster and the denominator. Adding or removing a + // row without changing the owned length is a compiler error; no prose keeps + // a second history or count beside it. let checks: [Check; 18] = [ ("agents-claude-parity", check_agents_claude_parity), ("lf-and-no-symlinks", check_lf_and_no_symlinks), diff --git a/xtask/src/repository/cargo.rs b/xtask/src/repository/cargo.rs index b6b659d..39a50cb 100644 --- a/xtask/src/repository/cargo.rs +++ b/xtask/src/repository/cargo.rs @@ -11,7 +11,7 @@ //! a reader standing on it recognizes a spelling nobody thought of. //! //! **What Cargo NORMALIZES** is a different question, and only cargo answers it: -//! `cargo metadata --locked --format-version 1` reports, under +//! `cargo metadata --locked --no-deps --format-version 1` reports, under //! `packages[].dependencies`, each package's manifest dependency DECLARATIONS //! after cargo has normalized them — workspace inheritance applied, package //! identity settled, a rename separated from the key it is written at, the edge @@ -69,22 +69,23 @@ pub(crate) const MANIFEST_FILE: &str = "Cargo.toml"; /// The table a platform-conditional dependency table hangs beneath. const TARGET_TABLE: &str = "target"; -/// Everything the Cargo authorities established about this repository, read -/// once. +/// What the committed manifest bytes declare, decoded once. +/// +/// This is a pure projection of the committed file map. Cargo's live answer is +/// deliberately carried by [`CargoObservation`], because a process observation +/// does not become a committed fact merely by being taken beside one. pub(crate) struct CargoSnapshot { - /// What cargo normalized the manifests to, or why nobody asked it. - normalized: Read, /// Every `.toml` document in the tree, decoded by the decoder that owns /// TOML. Keyed by canonical path, so no reader spells one twice. documents: BTreeMap>, - /// Every dependency entry every committed manifest declares, in one list. - census: ManifestCensus, + /// Every dependency entry every committed manifest declares, or the exact + /// manifest failure that prevented the census. + census: Read, } impl CargoSnapshot { - /// Reads every TOML document in the tree, takes the census off them, and - /// asks cargo what it resolved. - pub(crate) fn read(root: &Path, files: &CanonicalFileMap) -> Self { + /// Reads every TOML document in the committed tree and takes the census. + pub(crate) fn read(files: &CanonicalFileMap) -> Self { let mut documents = BTreeMap::new(); for (path, fact) in files.iter() { if !path.extension_is("toml") { @@ -93,16 +94,7 @@ impl CargoSnapshot { documents.insert(path.clone(), decode(path, fact.text())); } let census = ManifestCensus::take(&documents); - Self { - normalized: ask_cargo(root, files), - documents, - census, - } - } - - /// What cargo normalized the manifests to, or why nobody asked it. - pub(crate) const fn normalized(&self) -> &Read { - &self.normalized + Self { documents, census } } /// One decoded TOML document, or the absence of the file that would carry @@ -116,12 +108,46 @@ impl CargoSnapshot { } } - /// Every dependency entry every committed manifest declares. - pub(crate) const fn census(&self) -> &ManifestCensus { + /// Every dependency entry every committed manifest declares, or why that + /// census could not be established. + pub(crate) const fn census(&self) -> &Read { &self.census } } +/// Cargo's live normalization of the checkout beside one committed snapshot. +/// +/// This is role-distinct from [`CargoSnapshot`]. The latter is a pure decoding +/// of Git-owned bytes; this value is what an external process reported while +/// the checkout was observed clean and stable around that process. Consumers +/// that need Cargo's normalization ask for this observation explicitly rather +/// than inheriting it from the committed file facts. +/// +/// The observation does not claim that Cargo's operational configuration, +/// environment, executable, or dependency cache is part of the committed tree. +/// Untracked and ignored repository paths are likewise outside the committed +/// file population even where Cargo itself may consult an operational input. +/// A check spending this value therefore reports a live Cargo observation, not +/// a pure projection of `HEAD`. +pub(crate) struct CargoObservation { + /// What cargo normalized the manifests to, or why nobody asked it. + normalized: Read, +} + +impl CargoObservation { + /// Asks cargo what it normalized the committed manifest population to. + pub(crate) fn read(root: &Path, files: &CanonicalFileMap) -> Self { + Self { + normalized: ask_cargo(root, files), + } + } + + /// What cargo normalized the manifests to, or why nobody asked it. + pub(crate) const fn normalized(&self) -> &Read { + &self.normalized + } +} + /// One TOML text, decoded, with the failure carried where it did not decode. fn decode(path: &CanonicalPath, text: &Read) -> Read { match *text { @@ -231,23 +257,28 @@ impl fmt::Display for DeclaredDependency { /// /// The census is taken ONCE, over every manifest at once, so no law can be /// judging a different set of entries than another. A number that moves here -/// moved because the tree moved. +/// moved because the tree moved. One unreadable manifest makes the aggregate +/// unreadable; it never contributes the empty edge set. pub(crate) struct ManifestCensus(Vec); impl ManifestCensus { /// The census of every decoded manifest, in manifest-path order and, within /// one manifest, in edge-kind then key order. - fn take(documents: &BTreeMap>) -> Self { + fn take(documents: &BTreeMap>) -> Read { let mut entries = Vec::new(); for (path, document) in documents { if path.file_name() != MANIFEST_FILE { continue; } - if let Read::Known(ref document) = *document { - entries.extend(dependency_declarations(path, document)); + match *document { + Read::Known(ref document) => { + entries.extend(dependency_declarations(path, document)); + } + Read::DeclaredAbsent(reason) => return Read::DeclaredAbsent(reason), + Read::Unreadable(ref failure) => return Read::Unreadable(failure.clone()), } } - Self(entries) + Read::Known(Self(entries)) } /// Every entry one manifest declares. @@ -458,19 +489,34 @@ pub(crate) fn declares_table(document: &toml::Table, key_path: &[&str]) -> Read< /// be an inventory nobody joins. #[derive(Debug, Deserialize)] pub(crate) struct NormalizedWorkspace { - /// Every package cargo reported, workspace members included. This is the - /// manifest census as cargo normalized it, and not the selected graph — see - /// this module's own documentation for why that is the subject wanted here. + /// Every workspace package cargo reported under `--no-deps`. This is the + /// workspace manifest census as cargo normalized it, and not the selected + /// graph — see this module's own documentation for why that is the subject + /// wanted here. packages: Vec, } impl NormalizedWorkspace { - /// The package cargo reported under one name, or the declared absence of - /// it. + /// The one workspace package cargo reported under a name. + /// + /// Zero matches are declared absence. More than one match is unreadable: + /// terminal spelling cannot choose an identity, and selecting the first + /// would make Cargo's output order an ambient authority over the verdict. pub(crate) fn package(&self, named: &str) -> Read<&NormalizedPackage> { - match self.packages.iter().find(|package| package.name == named) { - Some(found) => Read::Known(found), - None => Read::DeclaredAbsent(AbsenceReason::NoSuchKey), + let mut matching = self.packages.iter().filter(|package| package.name == named); + let Some(found) = matching.next() else { + return Read::DeclaredAbsent(AbsenceReason::NoSuchKey); + }; + if matching.next().is_some() { + Read::Unreadable(ReadFailure::new( + "cargo metadata workspace package identity", + &format!( + "more than one workspace package is named `{named}`; terminal spelling cannot \ + select one" + ), + )) + } else { + Read::Known(found) } } } @@ -552,6 +598,7 @@ fn ask_cargo(root: &Path, files: &CanonicalFileMap) -> Read .args([ "metadata", "--locked", + "--no-deps", "--format-version", "1", "--manifest-path", @@ -574,7 +621,7 @@ fn ask_cargo(root: &Path, files: &CanonicalFileMap) -> Read match serde_json::from_slice::(&output.stdout) { Ok(reported) => Read::Known(reported), Err(error) => Read::Unreadable(ReadFailure::new( - "cargo metadata --format-version 1", + "cargo metadata --no-deps --format-version 1", &error.to_string(), )), } @@ -590,7 +637,10 @@ fn ask_cargo(root: &Path, files: &CanonicalFileMap) -> Read /// is proven against text, never against the tree it guards. #[cfg(test)] mod tests { - use super::{DeclaredDependency, EdgeKind, dependency_declarations, string_at, strings_at}; + use super::{ + DeclaredDependency, EdgeKind, NormalizedPackage, NormalizedWorkspace, + dependency_declarations, string_at, strings_at, + }; use crate::repository::snapshot::repository_snapshot; use crate::repository::types::{CanonicalPath, Read}; @@ -805,56 +855,60 @@ mod tests { Ok(()) } - /// The census this repository commits, entry for entry. - /// - /// A reader replacement moves numbers only where the TREE moved, and this is - /// where that is pinned rather than asserted. Nineteen entries across seven - /// committed manifests; the root manifest declares none of them, because - /// what it carries is a workspace POOL. + /// Package lookup has three states: no workspace member, exactly one, or + /// an identity collision. Cargo's output order cannot settle the third. + #[test] + fn normalized_package_lookup_refuses_terminal_name_collisions() { + let one = NormalizedPackage { + name: String::from("threadpak"), + dependencies: Vec::new(), + }; + let duplicate = NormalizedPackage { + name: String::from("threadpak"), + dependencies: Vec::new(), + }; + let unique = NormalizedWorkspace { + packages: vec![one], + }; + assert!(matches!(unique.package("missing"), Read::DeclaredAbsent(_))); + assert!(matches!(unique.package("threadpak"), Read::Known(_))); + + let collided = NormalizedWorkspace { + packages: vec![ + NormalizedPackage { + name: String::from("threadpak"), + dependencies: Vec::new(), + }, + duplicate, + ], + }; + assert!(matches!(collided.package("threadpak"), Read::Unreadable(_))); + } + + /// The real census preserves Cargo's package identity across a rename. /// - /// It was fifteen before the decoders were admitted, and the four that - /// arrived are the four this crate now reads through: `toml`, - /// `pulldown-cmark`, `serde`, and `serde_json`, every one of them an entry - /// of `xtask/Cargo.toml`. Nothing else moved — same manifests, same kinds, - /// same keys, same declared packages and paths — so the number moved exactly - /// where the tree did and nowhere else. + /// Structural control rather than a frozen population count: the committed + /// manifest map derives the denominator, this specimen proves a declaration + /// whose key and package differ survives that derivation, and the root + /// workspace pool remains outside the edge population. #[test] - fn the_committed_census_is_nineteen_entries() -> Result<(), String> { + fn the_real_census_preserves_a_renamed_consumer_identity() -> Result<(), String> { let snapshot = repository_snapshot()?; - let census: Vec = snapshot + let census = snapshot .cargo() .census() - .0 - .iter() - .map(|entry| { - format!( - "{} [{}] {} package={:?} path={:?}", - entry.manifest(), - entry.kind, - entry.key, - entry.package, - entry.path - ) - }) - .collect(); - assert_eq!(census.len(), 19, "{census:#?}"); + .required("the committed manifest dependency census")?; assert!( - census.iter().any(|entry| entry - == "xtask/fixtures/renamed-consumer/Cargo.toml [dependencies] tp \ - package=Some(\"threadpak\") path=Some(\"../../..\")"), - "{census:#?}" - ); - // The four the decoders brought, and no fifth. - assert_eq!( census + .of("xtask/fixtures/renamed-consumer/Cargo.toml") .iter() - .filter(|entry| entry.starts_with("xtask/Cargo.toml")) - .count(), - 6, - "{census:#?}" + .any(|entry| entry.key == "tp" + && entry.package.as_deref() == Some("threadpak") + && entry.path() == Some("../../..")), + "the committed renamed-consumer declaration lost its package identity" ); assert!( - snapshot.cargo().census().of("Cargo.toml").is_empty(), + census.of("Cargo.toml").is_empty(), "the root manifest's workspace pool was read as an edge" ); Ok(()) diff --git a/xtask/src/repository/markdown.rs b/xtask/src/repository/markdown.rs index 738ec2c..a961f91 100644 --- a/xtask/src/repository/markdown.rs +++ b/xtask/src/repository/markdown.rs @@ -569,30 +569,41 @@ pub(crate) fn obligation_ledger(document: &MarkdownDocument, home: &str) -> Read }; let mut records: Vec = Vec::new(); let mut offences: Vec = Vec::new(); + let mut current: Option = None; for line in block.body.lines() { match classify_line(line) { BlockLine::Item { key, value } => match key { - Some(RECORD_IDENTITY) => records.push(ObligationRecord { - id: value.to_owned(), - green: Vec::new(), - red: Vec::new(), - }), - Some(other) => offences.push(format!( - "{home}: an obligation record opens with `{other}:` rather than with \ - `{RECORD_IDENTITY}:`, so the rows written beneath it belong to no obligation \ - anything can name" - )), - None => offences.push(format!( - "{home}: an obligation record opens with `{value}`, which states no field at \ - all, so the rows written beneath it belong to no obligation anything can name" - )), + Some(RECORD_IDENTITY) => { + records.push(ObligationRecord { + id: value.to_owned(), + green: Vec::new(), + red: Vec::new(), + }); + current = records.len().checked_sub(1); + } + Some(other) => { + current = None; + offences.push(format!( + "{home}: an obligation record opens with `{other}:` rather than with \ + `{RECORD_IDENTITY}:`, so the rows written beneath it belong to no \ + obligation anything can name" + )); + } + None => { + current = None; + offences.push(format!( + "{home}: an obligation record opens with `{value}`, which states no field \ + at all, so the rows written beneath it belong to no obligation anything \ + can name" + )); + } }, BlockLine::Field { key, value } => { let row = match key { GREEN_FIELD | RED_FIELD => key, _ => continue, }; - let Some(record) = records.last_mut() else { + let Some(record) = current.and_then(|index| records.get_mut(index)) else { offences.push(format!( "{home}: a `{row}:` row stands outside every obligation record. This join \ reads rows through the record that declared them, so a row no record owns \ @@ -1000,6 +1011,79 @@ mod tests { Ok(()) } + /// Planted reversal: a malformed item ends the preceding record's + /// ownership. Both row kinds written beneath it are orphans, while the next + /// valid identity begins a fresh record normally. + #[test] + fn a_malformed_item_cannot_leave_the_previous_record_selected() -> Result<(), String> { + for malformed in [ + "- challenge_kind: compile-law", + "- a-keyless-obligation-item", + ] { + let document = MarkdownDocument::parse(&format!( + "```yaml\n\ + home: bounds\n\ + obligations:\n\ + \x20 - id: bounds.preceding\n\ + \x20 green: laws.rs bounds::preceding\n\ + \x20 red: owed-to-testpak\n\ + \x20 {malformed}\n\ + \x20 green: laws.rs bounds::orphaned\n\ + \x20 red: testpak/tests/orphaned.rs\n\ + \x20 - id: bounds.restored\n\ + \x20 green: laws.rs bounds::restored\n\ + \x20 red: owed-to-testpak\n\ + ```\n" + )); + let ledger = + obligation_ledger(&document, "home/README.md").taken("the obligation ledger")?; + assert_eq!(ledger.records().len(), 2, "{malformed}"); + assert_eq!( + ledger.offences().len(), + 3, + "{malformed}: {:?}", + ledger.offences() + ); + assert_eq!( + ledger + .offences() + .iter() + .filter(|offence| offence.contains("stands outside every obligation record")) + .count(), + 2, + "{malformed}: {:?}", + ledger.offences() + ); + assert!( + ledger + .records() + .iter() + .all(|record| record.green.len() == 1 && record.red.len() == 1), + "{malformed}: a valid record lost or gained a row" + ); + assert!( + !ledger + .records() + .iter() + .flat_map(|record| &record.green) + .any(|row| matches!( + row, + GreenRow::CompileTimeSeat { law, .. } if law == "orphaned" + )), + "{malformed}: the malformed item's green row attached to a valid record" + ); + assert!( + !ledger + .records() + .iter() + .flat_map(|record| &record.red) + .any(|row| row.contains("orphaned")), + "{malformed}: the malformed item's red row attached to a valid record" + ); + } + Ok(()) + } + /// A document declaring one schema TWICE is refused rather than resolved by /// position. #[test] diff --git a/xtask/src/repository/mod.rs b/xtask/src/repository/mod.rs index 2655ff0..0a207cc 100644 --- a/xtask/src/repository/mod.rs +++ b/xtask/src/repository/mod.rs @@ -6,10 +6,12 @@ //! be proven against fixture text instead of against the tree it guards. //! //! Each module is authoritative for one language and claims nothing outside it: -//! [`cargo`] for Cargo's syntax and for what cargo normalizes those manifests -//! to, [`markdown`] for document structure, [`rust`] for Rust syntax. -//! [`snapshot`] is the one place that touches the filesystem or starts a -//! process; [`types`] is the vocabulary the readings and the laws share. +//! [`cargo`] for Cargo's syntax and for the role-distinct live observation of +//! what cargo normalizes those manifests to, [`markdown`] for document +//! structure, [`rust`] for Rust syntax. [`snapshot`] owns the Git-derived +//! committed population and orchestrates the aggregate read; [`cargo`] owns its +//! live Cargo process; [`types`] is the vocabulary the readings and the laws +//! share. //! //! The modules are declared in dependency order: the shared vocabulary, then the //! snapshot every reading is carried in, then the three decoders. diff --git a/xtask/src/repository/snapshot.rs b/xtask/src/repository/snapshot.rs index 8da28f2..fb805e2 100644 --- a/xtask/src/repository/snapshot.rs +++ b/xtask/src/repository/snapshot.rs @@ -1,7 +1,9 @@ -//! One reading of the repository, built once, consumed by every law. +//! One committed reading of the repository, built once, consumed by every law. //! -//! This is the only module in this crate that touches the filesystem or starts a -//! process. Everything downstream is a pure function over what this established. +//! This module orchestrates the aggregate read and owns the Git processes that +//! derive committed membership and bytes. The Cargo reader owns its separate +//! live Cargo process. Everything downstream is a pure function over the facts +//! those role-distinct readers established. //! //! # Why one reading rather than many //! @@ -21,46 +23,26 @@ //! string for a manifest, an empty vector for a tree, `"."` for a root nobody //! resolved. Each of those answers the question the reader never got to ask, and //! answers it in the direction that reports clean about bytes nobody opened. -//! Every fact here is a [`Read`], so a law either handles the absence or is -//! refused by [`Read::required`]; there is no method in this crate that turns an -//! unread fact into a value. +//! Every fallible file projection is a [`Read`], so a law either handles the +//! absence or is refused by [`Read::required`]; there is no method in this crate +//! that turns an unread fact into a value. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::fmt; -use std::fs; +use std::io::{BufRead, BufReader, Read as IoRead, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use crate::repository::cargo::CargoSnapshot; +use crate::repository::cargo::{CargoObservation, CargoSnapshot}; use crate::repository::markdown::MarkdownSnapshot; use crate::repository::rust::RustSyntaxSnapshot; use crate::repository::types::{AbsenceReason, CanonicalPath, LinkState, Read, ReadFailure}; -/// Git's own storage, AT THE REPOSITORY ROOT. -/// -/// A directory in an ordinary clone and a FILE in a git worktree, where it -/// carries one line naming where the real storage lives. Both are git's, and -/// both are excluded here — otherwise one committed tree yields two different -/// populations depending on how the checkout was made, and every agent on this -/// campaign works in a worktree, so that is the live case rather than the exotic -/// one. It was live: the worktree's `.git` FILE was in the map, because the -/// exclusion asked whether an entry was a DIRECTORY before it asked what it was -/// called. +/// Git's own storage coordinate at a checkout root. const GIT_STORAGE: &str = ".git"; -/// The build's output directory, AT THE REPOSITORY ROOT. -/// -/// Cargo's output for this workspace, which is not repository material and is -/// large enough that walking it would make every run pay for bytes no law is -/// about. Only the root one: `target` is an ordinary word, and excluding it by -/// BASENAME at every depth silently deleted `src//target/`, -/// `docs/target/`, and `testpak/target/` from a population no law would have -/// reported missing. A file called `target` is not a build directory and is -/// read like any other file. -const BUILD_OUTPUT: &str = "target"; - /// The metaprogramming subsystem's directory. /// /// Four laws name it, so the name answers to no single law and stands here @@ -79,53 +61,74 @@ pub(crate) const MACHINE_DIRECTORY: &str = "src"; /// a path, which is what makes "no law walks the tree for itself" a fact of the /// types rather than a convention somebody keeps. pub(crate) struct RepositorySnapshot { - /// Every file in the tree, read once, with its bytes and its text. + /// Every blob in the committed Git tree, read once, with its bytes and text. files: CanonicalFileMap, - /// What Cargo's two authorities established. + /// What the committed TOML bytes declare. cargo: CargoSnapshot, + /// What a live Cargo process reported beside this committed reading. + cargo_observation: CargoObservation, /// Every Rust source, parsed once. rust: RustSyntaxSnapshot, /// Every Markdown document, parsed once. markdown: MarkdownSnapshot, - /// What the bytes read are bound to, established AROUND the read rather - /// than asked afterwards. - binding: CommitBinding, + /// The commit and tree that own every byte in `files`. + committed: CommittedTree, } impl RepositorySnapshot { /// Reads the repository at one root. /// - /// The order is the dependency order of the readings, and the git readings - /// BRACKET the walk rather than following it. The walk used to happen first - /// and git was asked afterwards, so a run printed a commit-bound sentence - /// about bytes it had never compared to that commit: on a dirty tree the - /// sentence named a commit whose content was not what had been read, and a - /// commit that moved mid-walk left the reading a mixture of two trees with - /// nothing saying so. Now git is asked before the walk and again after it, - /// the checkout is asked what differs, and [`CommitBinding::establish`] - /// either names the committed tree these bytes ARE or states what stops - /// them from being one. A commit that moved between the two readings - /// refuses the whole reading, because those bytes are about no single tree. + /// Git owns both membership and bytes: `ls-tree -z` derives the exact blob + /// population and `cat-file --batch` reads those immutable objects. Ignored + /// and untracked filesystem entries therefore cannot enter this type. A + /// tracked checkout difference or a moving `HEAD` refuses construction. /// - /// The decoders come after the binding on purpose. `cargo metadata` starts - /// a process that writes into the build directory, and a reading that asked - /// what differs AFTER running it would be asking about a checkout its own - /// reading had touched. + /// Cargo metadata remains a live observation. The committed state and + /// tracked-clean posture are checked again after that process so a run does + /// not join a stable committed projection to an observation taken while the + /// checkout moved. pub(crate) fn read(root: &Path) -> Result { - let before = committed_tree(root); - let files = CanonicalFileMap::read(root)?; - let after = committed_tree(root); - let differences = working_tree_differences(root); - let binding = CommitBinding::establish(&before, &after, &differences)?; - let cargo = CargoSnapshot::read(root, &files); + Self::read_with_after_files(root, || Ok(())) + } + + /// The aggregate read with one private post-file-map observation seam. + /// + /// Production supplies a no-op. The snapshot's own planted reversal moves + /// `HEAD` here, after immutable bytes were read and before the second state + /// check, so deleting or misplacing the builder check makes that reversal + /// fail rather than leaving a helper-only test green. + fn read_with_after_files( + root: &Path, + after_files: impl FnOnce() -> Result<(), String>, + ) -> Result { + Self::read_with_hooks(root, || Ok(()), after_files) + } + + /// The aggregate read with deterministic seams around state and bytes. + fn read_with_hooks( + root: &Path, + after_initial_commit: impl FnOnce() -> Result<(), String>, + after_files: impl FnOnce() -> Result<(), String>, + ) -> Result { + let before = committed_tree_with_after_commit(root, after_initial_commit)?; + require_tracked_clean(root)?; + let files = CanonicalFileMap::read(root, &before.tree)?; + after_files()?; + require_same_committed_state(&before, &committed_tree(root)?)?; + require_tracked_clean(root)?; + let cargo = CargoSnapshot::read(&files); let rust = RustSyntaxSnapshot::read(&files); let markdown = MarkdownSnapshot::read(&files); + let cargo_observation = CargoObservation::read(root, &files); + require_same_committed_state(&before, &committed_tree(root)?)?; + require_tracked_clean(root)?; Ok(Self { files, cargo, + cargo_observation, rust, markdown, - binding, + committed: before, }) } @@ -139,6 +142,11 @@ impl RepositorySnapshot { &self.cargo } + /// What Cargo reported live beside this committed snapshot. + pub(crate) const fn cargo_observation(&self) -> &CargoObservation { + &self.cargo_observation + } + /// Every Rust source, parsed. pub(crate) const fn rust(&self) -> &RustSyntaxSnapshot { &self.rust @@ -149,9 +157,9 @@ impl RepositorySnapshot { &self.markdown } - /// What the bytes this reading carries are bound to. - pub(crate) const fn binding(&self) -> &CommitBinding { - &self.binding + /// The commit and tree that own every byte this snapshot carries. + pub(crate) const fn committed(&self) -> &CommittedTree { + &self.committed } } @@ -163,15 +171,10 @@ pub(crate) struct CanonicalFileMap { } impl CanonicalFileMap { - /// Reads every file under one root, entering everything except - /// [`GIT_STORAGE`] and [`BUILD_OUTPUT`] AT THAT ROOT. - /// - /// A directory that cannot be listed refuses the whole reading. What such a - /// directory contains is unknown, and a snapshot built around an unknown is - /// a snapshot every law downstream would report clean about. - fn read(root: &Path) -> Result { - let mut entries = BTreeMap::new(); - read_directory(root, "", &mut entries)?; + /// Reads the exact blob population of one immutable Git tree. + fn read(root: &Path, tree: &TreeId) -> Result { + let tracked = tracked_blobs(root, tree)?; + let entries = read_tracked_blobs(root, &tracked)?; Ok(Self { entries }) } @@ -228,10 +231,8 @@ impl CanonicalFileMap { /// What one reading of one file established. pub(crate) struct FileFact { - /// Whether the entry is a symbolic link, or why that could not be - /// established. An entry the platform would not describe is UNKNOWN rather - /// than an ordinary file: a law about symlinks answering "ordinary" about an - /// entry nobody could stat is the fallback this model exists to delete. + /// Whether the committed Git mode names a symbolic link. An unsupported + /// mode refuses construction instead of arriving here as an ordinary file. link: Read, /// The bytes, or why they were not read. bytes: Read>, @@ -256,136 +257,224 @@ impl FileFact { } } -/// Reads one directory into the map, recursing in file-name order. -/// -/// The canonical spelling is BUILT on the way down rather than recovered on the -/// way back: each level appends the name it just read to the spelling it was -/// handed. Stripping a root off an absolute path afterwards would be a second -/// derivation of a fact this walk already has, and it would need a fallback for -/// the case it cannot happen in. -/// -/// # The two exclusions are ROOT-RELATIVE, and one of them ignores kind -/// -/// Being at the root is a fact this walk already has — `inside` is empty there -/// and nowhere else — so the two exclusions are asked exactly where they mean -/// something. Asked by BASENAME at every depth, as they were, `target` -/// disappeared a directory of repository material anywhere in the tree that -/// happened to carry the build's name, and no law would have reported the -/// absence, because a law is about the population it was handed. -/// -/// [`GIT_STORAGE`] is excluded whatever KIND the entry is, and that asymmetry is -/// the worktree repair: in a clone it is a directory and the old reading skipped -/// it, in a worktree it is a file and the old reading read it into the map. One -/// committed tree, two populations, decided by how somebody checked it out. -fn read_directory( - directory: &Path, - inside: &str, - into: &mut BTreeMap, -) -> Result<(), String> { - let listing = fs::read_dir(directory).map_err(|e| format!("{}: {e}", directory.display()))?; - let mut found = Vec::new(); - for entry in listing { - let entry = entry.map_err(|e| format!("{}: {e}", directory.display()))?; - let path = entry.path(); - let kind = entry - .file_type() - .map_err(|e| format!("{}: {e}", path.display()))?; - found.push((entry.file_name(), path, kind.is_dir())); - } - found.sort_by(|(left, _, _), (right, _, _)| left.cmp(right)); - let at_root = inside.is_empty(); - for (name, path, is_directory) in found { - let named = canonical_name(&name) - .map_err(|refusal| format!("{}: {refusal}", directory.display()))?; - if at_root && named == GIT_STORAGE { - continue; +/// One blob Git lists in a committed tree. +struct TrackedBlob { + /// Git's canonical root-relative path spelling. + path: CanonicalPath, + /// The blob object that owns the bytes. + object: String, + /// Whether the tree entry is a regular file or a symbolic link. + link: LinkState, +} + +/// Derives the committed file population from Git's NUL-delimited tree format. +fn tracked_blobs(root: &Path, tree: &TreeId) -> Result, String> { + let output = git(root) + .args(["ls-tree", "-rz", "--full-tree"]) + .arg(tree.to_string()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("git ls-tree: {error}"))?; + if !output.status.success() { + return Err(format!( + "git ls-tree refused the committed population: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + parse_tracked_blobs(&output.stdout) +} + +/// Parses exactly the records `git ls-tree -rz` emits. +fn parse_tracked_blobs(output: &[u8]) -> Result, String> { + if output.last().is_some_and(|last| *last != 0) { + return Err(String::from( + "git ls-tree did not terminate its final path record with NUL", + )); + } + if output.windows(2).any(|pair| pair == b"\0\0") { + return Err(String::from("git ls-tree emitted an empty path record")); + } + let mut tracked = Vec::new(); + let mut seen = BTreeSet::new(); + for record in output + .split(|byte| *byte == 0) + .filter(|record| !record.is_empty()) + { + let (header, raw_path) = split_once_byte(record, b'\t').ok_or_else(|| { + format!( + "git ls-tree emitted a record without its path separator: `{}`", + String::from_utf8_lossy(record) + ) + })?; + let mut fields = header.split(|byte| *byte == b' '); + let mode = ascii_field(fields.next(), "mode")?; + let kind = ascii_field(fields.next(), "object kind")?; + let object = ascii_field(fields.next(), "object identity")?; + if fields.next().is_some() { + return Err(format!( + "git ls-tree emitted extra header fields for `{}`", + String::from_utf8_lossy(raw_path) + )); } - if at_root && is_directory && named == BUILD_OUTPUT { - continue; + if kind != "blob" { + return Err(format!( + "git tree entry `{}` is a `{kind}` rather than a blob; committed snapshots do not \ + invent file semantics for Git links or unknown object kinds", + String::from_utf8_lossy(raw_path) + )); } - let spelled = if at_root { - named - } else { - format!("{inside}/{named}") + let link = match mode { + "100644" | "100755" => LinkState::RegularFile, + "120000" => LinkState::Symlink, + other => { + return Err(format!( + "git tree entry `{}` carries unsupported blob mode `{other}`", + String::from_utf8_lossy(raw_path) + )); + } }; - if is_directory { - read_directory(&path, &spelled, into)?; - continue; + let spelled = std::str::from_utf8(raw_path).map_err(|error| { + format!( + "git tracks a path with no Unicode spelling (`{}`): {error}; every canonical path \ + is a join key against repository text, so lossy conversion cannot be identity", + String::from_utf8_lossy(raw_path) + ) + })?; + if spelled.contains('\\') { + return Err(format!( + "git tracks `{spelled}` with a literal backslash; canonical repository paths use \ + forward slashes, so replacing that byte would collapse two identities" + )); + } + let path = CanonicalPath::spelled(spelled); + if !seen.insert(path.clone()) { + return Err(format!("git listed committed path `{path}` more than once")); } - into.insert(CanonicalPath::spelled(&spelled), read_file(&path)); + tracked.push(TrackedBlob { + path, + object: object.to_owned(), + link, + }); } - Ok(()) + Ok(tracked) } -/// One entry name as this repository spells names, or the refusal that says it -/// cannot be spelled at all. -/// -/// # Lossy conversion cannot be identity, so this refuses instead -/// -/// The walk used to build canonical paths with `to_string_lossy`, which maps -/// every unpaired surrogate and every ill-formed byte onto one replacement -/// character. Two entries whose names differ only where the conversion is lossy -/// therefore produce ONE [`CanonicalPath`], and the second insertion silently -/// overwrites the first: a file leaves the population with no error anywhere, -/// which is the exact silence this model exists to end. A rendering is a thing -/// to show a person; it is not an identity, and it was being used as the join -/// key of every law in this crate. -/// -/// Of the two lawful repairs — refuse the path, or key on raw platform bytes and -/// render separately — this repository refuses, and the reason is what the key -/// is FOR. A [`CanonicalPath`] is joined against text somebody wrote: an -/// obligation row naming a route, an allowlist entry, a README's declared -/// member, a band map. Those documents are UTF-8, so a path that cannot be -/// spelled in them is a path no row can name and no join can resolve; keying on -/// platform bytes would mint a second spelling for every path in the tree while -/// leaving the unnameable ones exactly as unjoinable as they are now. Refusing -/// costs a repository that carries such a name one clear refusal naming the -/// directory it is in, and this one carries none. -fn canonical_name(name: &OsStr) -> Result { - match name.to_str() { - Some(named) => Ok(String::from(named)), - None => Err(format!( - "the entry rendering as `{}` is not Unicode, so it has no canonical path spelling. \ - Every path identity in this crate is a join key against text — obligation rows, \ - allowlists, declared members — and a lossy rendering is not an identity: two such \ - names collapse onto one key and one of them leaves the population with nothing \ - saying so", - name.to_string_lossy() - )), +/// Reads every committed blob through one batch process. +fn read_tracked_blobs( + root: &Path, + tracked: &[TrackedBlob], +) -> Result, String> { + let mut child = git(root) + .args(["cat-file", "--batch"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| format!("git cat-file --batch: {error}"))?; + let mut input = child + .stdin + .take() + .ok_or_else(|| String::from("git cat-file opened no batch input"))?; + let output = child + .stdout + .take() + .ok_or_else(|| String::from("git cat-file opened no batch output"))?; + let mut output = BufReader::new(output); + let mut entries = BTreeMap::new(); + for entry in tracked { + writeln!(input, "{}", entry.object) + .map_err(|error| format!("git cat-file batch input: {error}"))?; + input + .flush() + .map_err(|error| format!("git cat-file batch input flush: {error}"))?; + let mut header = Vec::new(); + output + .read_until(b'\n', &mut header) + .map_err(|error| format!("git cat-file batch header: {error}"))?; + if header.last() == Some(&b'\n') { + header.pop(); + } + let header = std::str::from_utf8(&header) + .map_err(|error| format!("git cat-file emitted a non-UTF-8 header: {error}"))?; + let mut fields = header.split(' '); + let reported_object = fields.next().unwrap_or_default(); + let kind = fields.next().unwrap_or_default(); + let size = fields + .next() + .ok_or_else(|| format!("git cat-file emitted malformed header `{header}`"))? + .parse::() + .map_err(|error| { + format!("git cat-file emitted malformed size in `{header}`: {error}") + })?; + if fields.next().is_some() || reported_object != entry.object || kind != "blob" { + return Err(format!( + "git cat-file reported `{header}` while `{}` was requested for `{}`", + entry.object, entry.path + )); + } + let mut bytes = vec![0_u8; size]; + output.read_exact(&mut bytes).map_err(|error| { + format!("git blob `{}` for `{}`: {error}", entry.object, entry.path) + })?; + let mut terminator = [0_u8; 1]; + output + .read_exact(&mut terminator) + .map_err(|error| format!("git blob `{}` terminator: {error}", entry.object))?; + if terminator.as_slice() != b"\n" { + return Err(format!( + "git cat-file did not terminate blob `{}` with its protocol newline", + entry.object + )); + } + let text = match std::str::from_utf8(&bytes) { + Ok(text) => Read::Known(text.to_owned()), + Err(error) => { + Read::Unreadable(ReadFailure::new(entry.path.as_str(), &error.to_string())) + } + }; + entries.insert( + entry.path.clone(), + FileFact { + link: Read::Known(entry.link), + bytes: Read::Known(bytes), + text, + }, + ); + } + drop(input); + let mut trailing = Vec::new(); + output + .read_to_end(&mut trailing) + .map_err(|error| format!("git cat-file trailing output: {error}"))?; + let completed = child + .wait_with_output() + .map_err(|error| format!("git cat-file completion: {error}"))?; + if !completed.status.success() { + return Err(format!( + "git cat-file refused committed blobs: {}", + String::from_utf8_lossy(&completed.stderr).trim() + )); + } + if !trailing.is_empty() { + return Err(format!( + "git cat-file emitted {} unexpected trailing byte(s)", + trailing.len() + )); } + Ok(entries) } -/// One file's facts. -fn read_file(path: &Path) -> FileFact { - let spelled = path.display().to_string(); - let link = match fs::symlink_metadata(path) { - Ok(metadata) => Read::Known(if metadata.file_type().is_symlink() { - LinkState::Symlink - } else { - LinkState::RegularFile - }), - Err(error) => Read::Unreadable(ReadFailure::new(&spelled, &error.to_string())), - }; - let bytes = match fs::read(path) { - Ok(bytes) => Read::Known(bytes), - Err(error) => { - let failure = ReadFailure::new(&spelled, &error.to_string()); - return FileFact { - link, - bytes: Read::Unreadable(failure.clone()), - text: Read::Unreadable(failure), - }; - } - }; - let text = match bytes { - Read::Known(ref bytes) => match std::str::from_utf8(bytes) { - Ok(text) => Read::Known(text.to_owned()), - Err(error) => Read::Unreadable(ReadFailure::new(&spelled, &error.to_string())), - }, - Read::DeclaredAbsent(reason) => Read::DeclaredAbsent(reason), - Read::Unreadable(ref failure) => Read::Unreadable(failure.clone()), - }; - FileFact { link, bytes, text } +/// Splits one byte slice at its first named byte. +fn split_once_byte(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> { + let at = bytes.iter().position(|byte| *byte == separator)?; + let (before, from_separator) = bytes.split_at(at); + Some((before, from_separator.get(1..)?)) +} + +/// One ASCII field from Git's machine-readable header. +fn ascii_field<'field>(field: Option<&'field [u8]>, named: &str) -> Result<&'field str, String> { + let field = field.ok_or_else(|| format!("git ls-tree omitted its {named}"))?; + std::str::from_utf8(field).map_err(|error| format!("git ls-tree {named} is not ASCII: {error}")) } /// The committed state git names at one moment: a commit and the tree it names. @@ -408,227 +497,94 @@ impl fmt::Display for CommittedTree { } } -/// Asks git what `HEAD` names, or states that this root is not a checkout. -/// -/// Called TWICE around the walk. What it names is a committed state, which is -/// not the same fact as what was read off the disk — the files map is what was -/// read — and keeping those two apart is the whole of [`CommitBinding`]. -fn committed_tree(root: &Path) -> Read { - if !root.join(GIT_STORAGE).exists() { - return Read::DeclaredAbsent(AbsenceReason::NotAGitCheckout); - } - let commit = match revision(root, "HEAD") { - Read::Known(named) => CommitId(named), - Read::DeclaredAbsent(reason) => return Read::DeclaredAbsent(reason), - Read::Unreadable(failure) => return Read::Unreadable(failure), - }; - let tree = match revision(root, "HEAD^{tree}") { - Read::Known(named) => TreeId(named), - Read::DeclaredAbsent(reason) => return Read::DeclaredAbsent(reason), - Read::Unreadable(failure) => return Read::Unreadable(failure), - }; - Read::Known(CommittedTree { commit, tree }) +/// Asks Git what `HEAD` names, refusing a root with no committed state. +fn committed_tree(root: &Path) -> Result { + committed_tree_with_after_commit(root, || Ok(())) } -/// Every path git reports as differing from what is committed, one entry per -/// line of `git status --porcelain`, each carrying git's two status columns. -/// -/// Empty output is the entire clean condition — git prints one line per path -/// that differs from `HEAD` or is untracked, and nothing at all when there is -/// none. Nothing here interprets a status column; the lines are carried whole so -/// a refusal can name what differs rather than count it. -/// -/// This is a reading of a PROCESS's output rather than of repository text, which -/// is why it is a line reading and why that is not the defect class this crate -/// has been deleting: git's porcelain format is a line-per-path contract, and -/// the only fact taken from it here is how many lines there are. -fn working_tree_differences(root: &Path) -> Read> { - let output = Command::new("git") - .current_dir(root) - .args(["status", "--porcelain"]) - .stderr(Stdio::piped()) - .output(); - let output = match output { - Ok(output) => output, - Err(error) => { - return Read::Unreadable(ReadFailure::new( - "git status --porcelain", - &error.to_string(), - )); - } - }; - if !output.status.success() { - return Read::Unreadable(ReadFailure::new( - "git status --porcelain", - String::from_utf8_lossy(&output.stderr).trim(), +/// Captures one commit/tree pair with a private deterministic challenge seam. +fn committed_tree_with_after_commit( + root: &Path, + after_commit: impl FnOnce() -> Result<(), String>, +) -> Result { + if !root.join(GIT_STORAGE).exists() { + return Err(format!( + "{} is not a Git checkout, so no committed repository snapshot can be read", + root.display() )); } - Read::Known( - String::from_utf8_lossy(&output.stdout) - .lines() - .filter(|line| !line.is_empty()) - .map(String::from) - .collect(), - ) + let commit = CommitId(revision(root, "HEAD")?); + after_commit()?; + let tree = TreeId(revision(root, &format!("{}^{{tree}}", commit.0))?); + Ok(CommittedTree { commit, tree }) } -/// What the bytes one reading carries are bound to. -/// -/// Two states, and the second one exists because the first was being CLAIMED -/// without being established. The reading walked the live filesystem, then asked -/// git what `HEAD` named, then printed `read N files at commit X` — a sentence -/// about a relationship between those bytes and that commit which nothing had -/// checked. On a dirty checkout the sentence was simply false, and `cargo xtask -/// check` alone never noticed: the worktree-clean stage that would have caught -/// it runs only under `qualify`, and only at the end. +/// Requires every tracked checkout path to match `HEAD`. /// -/// A verdict that cannot be attached to a tree is a verdict about nothing in -/// particular. So a reading either establishes the attachment or says out loud -/// that it has none — it never prints a commit it did not bind. -pub(crate) enum CommitBinding { - /// The bytes ARE this committed tree: git named the same commit and the - /// same tree on both sides of the walk, and the checkout carried nothing - /// differing from them. - Bound(CommittedTree), - /// The bytes are the working tree's, and this is what stops them from being - /// a committed tree. - Unbound(UnboundReason), -} - -impl CommitBinding { - /// The binding two git readings and one checkout reading establish, or the - /// refusal that says the reading is about no single tree. - /// - /// Pure over its three inputs, which is what lets the sentence a run opens - /// with be proven against fixture readings: a binding that could only be - /// tested by moving a commit under a running walk would never be tested. - /// - /// # What a bound reading establishes, and what it does not - /// - /// `git status --porcelain` reports every TRACKED path differing from `HEAD` - /// and every UNTRACKED path, and reports nothing for a path git IGNORES. The - /// walk enters every directory except the root's two, so an ignored - /// directory deeper in the tree would enter the file map without moving this - /// verdict. This repository ignores exactly one directory, and it is the - /// root build output the walk already refuses to enter, so the gap is empty - /// here and is stated rather than left to be discovered. It closes when the - /// walk's path set comes from git rather than from a filesystem listing — - /// which is a different reading, not a stricter version of this one. - fn establish( - before: &Read, - after: &Read, - differences: &Read>, - ) -> Result { - if before != after { - return Err(format!( - "the committed state moved while the repository was being read: git named {} \ - before the walk and {} after it, so the bytes this reading carries are a mixture \ - of two trees and no verdict over them is about either one", - named(before), - named(after) - )); - } - match *before { - Read::Known(ref committed) => match *differences { - Read::Known(ref entries) if entries.is_empty() => { - Ok(CommitBinding::Bound(committed.clone())) - } - Read::Known(ref entries) => Ok(CommitBinding::Unbound( - UnboundReason::WorkingTreeDiffers(entries.len()), - )), - Read::DeclaredAbsent(reason) => Ok(CommitBinding::Unbound( - UnboundReason::GitSaysNothing(reason), - )), - Read::Unreadable(ref failure) => Ok(CommitBinding::Unbound( - UnboundReason::GitRefused(failure.clone()), - )), - }, - Read::DeclaredAbsent(reason) => Ok(CommitBinding::Unbound( - UnboundReason::GitSaysNothing(reason), - )), - Read::Unreadable(ref failure) => Ok(CommitBinding::Unbound(UnboundReason::GitRefused( - failure.clone(), - ))), - } - } -} - -impl fmt::Display for CommitBinding { - fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - CommitBinding::Bound(ref committed) => write!( - out, - "and they are the committed tree at commit {} (tree {})", - committed.commit, committed.tree - ), - CommitBinding::Unbound(ref reason) => write!( - out, - "from the WORKING TREE; they are not a committed tree, so nothing this run reports \ - is bound to a commit: {reason}" - ), - } +/// Untracked and ignored paths are deliberately excluded rather than refused: +/// membership and bytes come from the committed tree, so neither population can +/// enter the snapshot. The `-z` porcelain contract is used so a tracked path +/// containing a newline cannot hide by changing record boundaries. +fn require_tracked_clean(root: &Path) -> Result<(), String> { + let output = git(root) + .args(["status", "--porcelain=v2", "-z", "--untracked-files=no"]) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("git status --porcelain=v2: {error}"))?; + if !output.status.success() { + return Err(format!( + "git could not establish tracked checkout cleanliness: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); } -} - -/// Why a reading's bytes are not a committed tree. -/// -/// Every variant is a statement somebody can act on: commit something, ask in a -/// checkout, or repair whatever refused. None of them is a shrug, and none of -/// them is an excuse to print a commit anyway. -pub(crate) enum UnboundReason { - /// The checkout carries paths differing from what is committed. - WorkingTreeDiffers(usize), - /// Git declared there is nothing to name here. - GitSaysNothing(AbsenceReason), - /// Git was asked and refused. - GitRefused(ReadFailure), -} - -impl fmt::Display for UnboundReason { - fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - UnboundReason::WorkingTreeDiffers(entries) => write!( - out, - "git reports {entries} path(s) in this checkout differing from what is committed" - ), - UnboundReason::GitSaysNothing(reason) => write!(out, "{reason}"), - UnboundReason::GitRefused(ref failure) => write!(out, "{failure}"), - } + if output.stdout.is_empty() { + Ok(()) + } else { + Err(String::from( + "tracked checkout bytes differ from HEAD; a committed snapshot refuses rather than \ + report about different live bytes beside the committed tree", + )) } } -/// How one read fact is named in a refusal about it. -fn named(read: &Read) -> String { - match *read { - Read::Known(ref fact) => fact.to_string(), - Read::DeclaredAbsent(reason) => reason.to_string(), - Read::Unreadable(ref failure) => failure.to_string(), +/// Refuses a read whose committed state moved at any point in the operation. +fn require_same_committed_state( + before: &CommittedTree, + after: &CommittedTree, +) -> Result<(), String> { + if before == after { + Ok(()) + } else { + Err(format!( + "the committed state moved while the repository was being read: Git named {before} \ + before the read and {after} after it" + )) } } /// The object one revision names. -fn revision(root: &Path, spelling: &str) -> Read { - let output = Command::new("git") - .current_dir(root) +fn revision(root: &Path, spelling: &str) -> Result { + let output = git(root) .args(["rev-parse", spelling]) .stderr(Stdio::piped()) - .output(); - let output = match output { - Ok(output) => output, - Err(error) => { - return Read::Unreadable(ReadFailure::new( - &format!("git rev-parse {spelling}"), - &error.to_string(), - )); - } - }; + .output() + .map_err(|error| format!("git rev-parse {spelling}: {error}"))?; if !output.status.success() { - return Read::Unreadable(ReadFailure::new( - &format!("git rev-parse {spelling}"), - String::from_utf8_lossy(&output.stderr).trim(), + return Err(format!( + "git rev-parse {spelling}: {}", + String::from_utf8_lossy(&output.stderr).trim() )); } - Read::Known(String::from_utf8_lossy(&output.stdout).trim().to_owned()) + let reported = std::str::from_utf8(&output.stdout) + .map_err(|error| format!("git rev-parse {spelling} emitted non-UTF-8 output: {error}"))?; + Ok(reported.trim().to_owned()) +} + +/// One Git command with replacement-object rewriting disabled. +fn git(root: &Path) -> Command { + let mut command = Command::new("git"); + command.current_dir(root).env("GIT_NO_REPLACE_OBJECTS", "1"); + command } /// The commit a reading was taken at. @@ -703,233 +659,411 @@ pub(crate) fn repository_snapshot() -> Result<&'static RepositorySnapshot, Strin /// Planted reversals for the reading itself. #[cfg(test)] mod tests { - use std::ffi::{OsStr, OsString}; - - use super::{ - BUILD_OUTPUT, CommitBinding, CommitId, CommittedTree, GIT_STORAGE, TreeId, canonical_name, - repository_snapshot, - }; - use crate::repository::types::{AbsenceReason, Read, ReadFailure}; - - /// One synthetic committed state. - fn committed(commit: &str, tree: &str) -> Read { - Read::Known(CommittedTree { - commit: CommitId(String::from(commit)), - tree: TreeId(String::from(tree)), - }) - } + use std::collections::BTreeSet; + use std::fs; + use std::io::Write as _; + use std::path::{Path, PathBuf}; + use std::process::{Command, Stdio}; + use std::sync::atomic::{AtomicUsize, Ordering}; - /// One synthetic checkout reading listing the paths that differ. - fn differing(paths: &[&str]) -> Read> { - Read::Known(paths.iter().map(|path| (*path).to_string()).collect()) + use super::{GIT_STORAGE, RepositorySnapshot, parse_tracked_blobs}; + use crate::checks::hygiene::check_lf_and_no_symlinks; + use crate::repository::types::{LinkState, Read}; + + /// One isolated Git repository for a committed-snapshot control. + struct GitFixture { + root: PathBuf, } - /// The reading names what it read, and never names a commit it did not - /// bind. - /// - /// Read against the real tree, which is dirty exactly when somebody is - /// working in it — so this states the rule that holds in BOTH states rather - /// than a fact about one of them. A bound reading names a real committed - /// state; an unbound one accounts for itself and its sentence carries no - /// commit at all. - #[test] - fn the_reading_never_names_a_commit_it_did_not_bind() -> Result<(), String> { - let snapshot = repository_snapshot()?; - let sentence = snapshot.binding().to_string(); - match *snapshot.binding() { - CommitBinding::Bound(ref committed) => { - assert_eq!(committed.commit.to_string().len(), 40, "{committed}"); - assert_eq!(committed.tree.to_string().len(), 40, "{committed}"); - assert!( - sentence.contains(&committed.commit.to_string()), - "{sentence}" - ); + impl GitFixture { + /// Creates a repository with deterministic local identity and byte rules. + fn named(name: &str) -> Result { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let ordinal = NEXT.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "threadpak-snapshot-{}-{ordinal}-{name}", + std::process::id() + )); + let _removed = fs::remove_dir_all(&root); + fs::create_dir_all(&root).map_err(|error| format!("{}: {error}", root.display()))?; + let fixture = Self { root }; + fixture.git(&["init", "--quiet"])?; + fixture.git(&["config", "user.name", "ThreadPak snapshot fixture"])?; + fixture.git(&["config", "user.email", "fixture@threadpak.invalid"])?; + fixture.git(&["config", "core.autocrlf", "false"])?; + fixture.git(&["config", "core.symlinks", "false"])?; + Ok(fixture) + } + + /// Writes one fixture path with exact bytes. + fn write(&self, relative: &str, bytes: &[u8]) -> Result<(), String> { + let path = self.root.join(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("{}: {error}", parent.display()))?; } - CommitBinding::Unbound(ref reason) => { - assert!( - sentence.contains("not a committed tree"), - "an unbound reading did not say so: {sentence}" - ); - assert!( - !sentence.contains("at commit"), - "an unbound reading named a commit anyway: {sentence}" - ); - assert!(!reason.to_string().is_empty(), "{sentence}"); + fs::write(&path, bytes).map_err(|error| format!("{}: {error}", path.display())) + } + + /// Commits the fixture's current tracked population. + fn commit(&self) -> Result<(), String> { + self.git(&["add", "-A"])?; + self.commit_index() + } + + /// Commits the index without restaging the working tree. + fn commit_index(&self) -> Result<(), String> { + self.git(&[ + "commit", + "--quiet", + "--allow-empty", + "--message", + "snapshot fixture", + ]) + } + + /// Stages one symbolic-link tree entry without asking the host to mint it. + fn stage_symlink(&self, relative: &str, target: &str) -> Result<(), String> { + let mut child = Command::new("git") + .current_dir(&self.root) + .env("GIT_NO_REPLACE_OBJECTS", "1") + .args(["hash-object", "-w", "--stdin"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| format!("git hash-object: {error}"))?; + child + .stdin + .take() + .ok_or_else(|| String::from("git hash-object opened no input"))? + .write_all(target.as_bytes()) + .map_err(|error| format!("git hash-object input: {error}"))?; + let output = child + .wait_with_output() + .map_err(|error| format!("git hash-object completion: {error}"))?; + if !output.status.success() { + return Err(format!( + "git hash-object refused: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); } + let object = std::str::from_utf8(&output.stdout) + .map_err(|error| format!("git hash-object emitted non-UTF-8 output: {error}"))? + .trim(); + let cache = format!("120000,{object},{relative}"); + self.git(&["update-index", "--add", "--cacheinfo", &cache]) + } + + /// Runs one Git operation and carries its refusal. + fn git(&self, arguments: &[&str]) -> Result<(), String> { + run_git(&self.root, arguments) + } + + /// Reads the committed snapshot. + fn snapshot(&self) -> Result { + RepositorySnapshot::read(&self.root) } - Ok(()) } - /// Planted reversal: the commit moving under the walk. - /// - /// THE failure the bracketing exists for. The reading used to walk the disk - /// and ask git afterwards, so a commit that moved mid-walk left the file map - /// carrying bytes from two trees while the sentence named whichever tree - /// happened to be current when the walk finished. Those bytes are about no - /// single tree, so the reading refuses rather than picking one. + impl Drop for GitFixture { + fn drop(&mut self) { + let _removed = fs::remove_dir_all(&self.root); + } + } + + /// Runs one Git command at a named root. + fn run_git(root: &Path, arguments: &[&str]) -> Result<(), String> { + let output = Command::new("git") + .current_dir(root) + .env("GIT_NO_REPLACE_OBJECTS", "1") + .args(arguments) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("git {}: {error}", arguments.join(" ")))?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "git {} refused: {}", + arguments.join(" "), + String::from_utf8_lossy(&output.stderr).trim() + )) + } + } + + /// The committed population and bytes, in stable order. + fn facts(snapshot: &RepositorySnapshot) -> Result, LinkState)>, String> { + snapshot + .files() + .iter() + .map(|(path, fact)| { + Ok(( + path.as_str().to_owned(), + fact.bytes().required(path.as_str())?.clone(), + *fact.link().required(path.as_str())?, + )) + }) + .collect() + } + + /// Positive control: committed files are read from their Git blobs. #[test] - fn a_commit_that_moves_under_the_walk_refuses_the_reading() { - let found = CommitBinding::establish( - &committed("aaaa", "bbbb"), - &committed("cccc", "dddd"), - &differing(&[]), + fn a_clean_committed_tree_is_read_from_git() -> Result<(), String> { + let fixture = GitFixture::named("clean")?; + fixture.write("nested/file.txt", b"committed bytes\n")?; + fixture.commit()?; + let snapshot = fixture.snapshot()?; + assert_eq!( + snapshot + .files() + .bytes("nested/file.txt") + .required("nested/file.txt")?, + b"committed bytes\n" ); + assert!(!snapshot.committed().to_string().is_empty()); + Ok(()) + } + + /// A large committed roster completes through the one-request/one-response + /// protocol. Writing every request before reading any response can fill the + /// two pipes in opposite directions and never reach a verdict. + #[test] + fn a_many_blob_roster_completes_without_cross_pipe_blocking() -> Result<(), String> { + let fixture = GitFixture::named("many-blobs")?; + let body = vec![b'x'; 1_024]; + let mut expected = BTreeSet::new(); + for ordinal in 0..2_048_u16 { + let path = format!("many/{ordinal:04}.txt"); + fixture.write(&path, &body)?; + expected.insert(path); + } + fixture.commit()?; + + let snapshot = fixture.snapshot()?; + let found: BTreeSet<_> = snapshot + .files() + .iter() + .map(|(path, _)| path.as_str().to_owned()) + .collect(); + assert_eq!(found, expected); + Ok(()) + } + + /// Git's symlink mode reaches the existing no-symlink law on every host. + #[test] + fn a_committed_symlink_mode_reaches_the_no_symlink_law() -> Result<(), String> { + let fixture = GitFixture::named("symlink-mode")?; + fixture.stage_symlink("link", "target.txt")?; + fixture.commit_index()?; + fixture.git(&["checkout", "--", "link"])?; + let snapshot = fixture.snapshot()?; + assert!(matches!( + snapshot.files().get("link").map(super::FileFact::link), + Some(Read::Known(LinkState::Symlink)) + )); + let found = check_lf_and_no_symlinks(&snapshot); assert!( - found.is_err_and(|refusal| refusal - .contains("moved while the repository was being read") - && refusal.contains("aaaa") - && refusal.contains("cccc")), - "a reading spanning two trees was accepted" + found.is_err_and(|refusal| refusal.contains("symlink: link")), + "Git mode 120000 did not activate the no-symlink law" ); + Ok(()) } - /// Planted reversal: a dirty checkout is NOT a commit-bound reading. - /// - /// The sentence `cargo xtask check` opens with used to name a commit on any - /// tree at all, and only `qualify`'s closing stage — which runs last, and - /// only under `qualify` — would eventually notice. The bytes read on a dirty - /// tree are the working tree's, and saying so is the honest state. + /// Moving `HEAD` inside the aggregate read refuses the constructor. #[test] - fn a_dirty_checkout_is_unbound_and_its_sentence_names_no_commit() -> Result<(), String> { - let state = committed( - "0123456789012345678901234567890123456789", - "9876543210987654321098765432109876543210", - ); - let found = CommitBinding::establish(&state, &state, &differing(&[" M src/lib.rs"]))?; - let sentence = found.to_string(); + fn a_moving_committed_state_refuses() -> Result<(), String> { + let fixture = GitFixture::named("moving-head")?; + fixture.write("first.txt", b"first\n")?; + fixture.commit()?; + let found = RepositorySnapshot::read_with_after_files(&fixture.root, || { + fixture.write("second.txt", b"second\n")?; + fixture.commit() + }); assert!( - !sentence.contains("0123456789"), - "an unbound reading named the commit anyway: {sentence}" + found.is_err_and(|refusal| refusal.contains("committed state moved")), + "the aggregate builder accepted a read spanning two committed states" ); - assert!(sentence.contains("1 path(s)"), "{sentence}"); Ok(()) } - /// The positive control: a clean checkout at a settled commit IS bound, and - /// its sentence names the tree the bytes are. + /// Moving `HEAD` between commit and tree capture cannot mint a mixed pair. #[test] - fn a_clean_checkout_at_one_commit_is_bound() -> Result<(), String> { - let state = committed( - "0123456789012345678901234567890123456789", - "9876543210987654321098765432109876543210", + fn commit_and_tree_capture_remain_one_aggregate_fact() -> Result<(), String> { + let fixture = GitFixture::named("mixed-pair")?; + fixture.write("first.txt", b"first\n")?; + fixture.commit()?; + let first = super::committed_tree(&fixture.root)?; + fixture.write("second.txt", b"second\n")?; + fixture.commit()?; + let second = super::committed_tree(&fixture.root)?; + fixture.git(&["reset", "--hard", &first.commit.to_string()])?; + + let found = RepositorySnapshot::read_with_hooks( + &fixture.root, + || fixture.git(&["reset", "--hard", &second.commit.to_string()]), + || Ok(()), ); - let found = CommitBinding::establish(&state, &state, &differing(&[]))?; - let sentence = found.to_string(); + let refusal = found + .err() + .ok_or_else(|| String::from("a state moving during aggregate capture was accepted"))?; assert!( - sentence.contains("0123456789012345678901234567890123456789"), - "{sentence}" + refusal.contains(&format!("Git named {first} before the read")) + && refusal.contains(&format!("and {second} after it")), + "the aggregate refusal carried a mixed commit/tree pair: {refusal}" ); + Ok(()) + } + + /// A tracked checkout difference refuses the committed constructor. + #[test] + fn tracked_dirt_refuses_the_committed_snapshot() -> Result<(), String> { + let fixture = GitFixture::named("dirty")?; + fixture.write("tracked.txt", b"committed\n")?; + fixture.commit()?; + fixture.write("tracked.txt", b"different\n")?; + let found = fixture.snapshot(); assert!( - sentence.contains("9876543210987654321098765432109876543210"), - "{sentence}" + found.is_err_and(|refusal| refusal.contains("tracked checkout bytes differ")), + "tracked dirt entered a committed snapshot" ); Ok(()) } - /// Planted reversal: a root that is no checkout, and a git that refused. - /// - /// Both used to print `unknown (…)` beside the word `commit` and carry on, - /// which is a run claiming a commit-bound result while stating it has no - /// commit. Neither is bound now, and the sentence stops claiming one. + /// Root and nested ignored bytes are outside the committed population. #[test] - fn an_unknown_commit_binds_nothing() -> Result<(), String> { - let absent: Read = Read::DeclaredAbsent(AbsenceReason::NotAGitCheckout); - let sentence = CommitBinding::establish(&absent, &absent, &differing(&[]))?.to_string(); - assert!(sentence.contains("not a committed tree"), "{sentence}"); - assert!(sentence.contains("not a git checkout"), "{sentence}"); - - let refused: Read = - Read::Unreadable(ReadFailure::new("git rev-parse HEAD", "no such ref")); - let said = CommitBinding::establish(&refused, &refused, &differing(&[]))?.to_string(); - assert!(said.contains("not a committed tree"), "{said}"); - assert!(said.contains("no such ref"), "{said}"); + fn ignored_files_never_enter_the_committed_snapshot() -> Result<(), String> { + let fixture = GitFixture::named("ignored")?; + fixture.write(".gitignore", b"root.ignored\nnested/ignored/\n")?; + fixture.write("tracked.txt", b"tracked\n")?; + fixture.commit()?; + fixture.write("root.ignored", b"ambient root\n")?; + fixture.write("nested/ignored/file.txt", b"ambient nested\n")?; + let snapshot = fixture.snapshot()?; + assert!(snapshot.files().get("root.ignored").is_none()); + assert!(snapshot.files().get("nested/ignored/file.txt").is_none()); + assert!(snapshot.files().get("tracked.txt").is_some()); Ok(()) } - /// Planted reversal: a name that has no Unicode spelling refuses the - /// reading rather than collapsing onto a replacement character. - /// - /// `to_string_lossy` maps every ill-formed name onto the same replacement - /// character, so two entries differing only there produced ONE canonical - /// path and the second insertion overwrote the first — a file leaving the - /// population with no error anywhere. The ill-formed name is built in the - /// platform's own terms, because there is no portable spelling of one; both - /// arms assert the same refusal, so whichever platform a run happens on, one - /// of them executes. + /// Untracked nonignored bytes are explicitly outside, not a hidden member. #[test] - fn a_name_with_no_unicode_spelling_refuses_the_reading() { - #[cfg(windows)] - let ill_formed = { - use std::os::windows::ffi::OsStringExt; - // An unpaired high surrogate: a name Windows accepts and UTF-8 - // cannot spell. - OsString::from_wide(&[0x0073_u16, 0xD800_u16, 0x0074_u16]) - }; - #[cfg(unix)] - let ill_formed = { - use std::os::unix::ffi::OsStringExt; - OsString::from_vec(vec![0x73_u8, 0xFF_u8, 0x74_u8]) - }; - assert!( - canonical_name(&ill_formed).is_err_and(|refusal| refusal.contains("is not Unicode")), - "a name with no Unicode spelling was given a canonical path anyway" - ); - assert_eq!( - canonical_name(OsStr::new("README.md")), - Ok(String::from("README.md")) - ); + fn untracked_files_are_outside_the_committed_snapshot() -> Result<(), String> { + let fixture = GitFixture::named("untracked")?; + fixture.write("tracked.txt", b"tracked\n")?; + fixture.commit()?; + fixture.write("untracked.txt", b"ambient\n")?; + let snapshot = fixture.snapshot()?; + assert!(snapshot.files().get("untracked.txt").is_none()); + assert!(snapshot.files().get("tracked.txt").is_some()); + Ok(()) } - /// The two unread entries are unread AT THE ROOT and nowhere else. - /// - /// Planted reversal for the basename rule, read off the real tree. Both - /// names are excluded where they mean what they say — git's storage and the - /// build's output, at the root — and neither is excluded as a WORD, so a - /// directory called `target` anywhere else in the tree is repository - /// material and is read like any other. + /// A non-Unicode Git path refuses instead of collapsing through lossy text. #[test] - fn the_root_exclusions_are_root_relative() -> Result<(), String> { - let snapshot = repository_snapshot()?; - let unread: Vec<&str> = snapshot - .files() - .iter() - .map(|(path, _)| path.as_str()) - .filter(|path| { - path.split('/') - .next() - .is_some_and(|head| head == GIT_STORAGE || head == BUILD_OUTPUT) - }) - .collect(); - assert!(unread.is_empty(), "{unread:?}"); + fn a_non_unicode_git_path_refuses() { + let found = parse_tracked_blobs(b"100644 blob abcdef\tbad\xffname\0"); assert!( - snapshot.files().get(GIT_STORAGE).is_none(), - "the worktree's `{GIT_STORAGE}` file is in the canonical file map, so this checkout \ - reads differently from a clone of the same commit" + found.is_err_and(|refusal| refusal.contains("no Unicode spelling")), + "a non-Unicode tracked path gained a canonical identity" ); + } + + /// A clone and linked worktree of one tree have one canonical population. + #[test] + fn clone_and_linked_worktree_read_identically() -> Result<(), String> { + let fixture = GitFixture::named("checkout-shapes")?; + fixture.write("a.txt", b"a\n")?; + fixture.write("nested/b.txt", b"b\n")?; + fixture.commit()?; + let parent = fixture + .root + .parent() + .ok_or_else(|| String::from("fixture root has no parent"))?; + let suffix = fixture + .root + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| String::from("fixture root has no Unicode name"))?; + let clone = parent.join(format!("{suffix}-clone")); + let worktree = parent.join(format!("{suffix}-worktree")); + let _clone_cleanup = fs::remove_dir_all(&clone); + let _worktree_removed = fs::remove_dir_all(&worktree); + let clone_output = Command::new("git") + .args(["clone", "--quiet"]) + .arg(&fixture.root) + .arg(&clone) + .output() + .map_err(|error| format!("git clone: {error}"))?; + if !clone_output.status.success() { + return Err(format!( + "git clone refused: {}", + String::from_utf8_lossy(&clone_output.stderr).trim() + )); + } + let worktree_output = Command::new("git") + .current_dir(&fixture.root) + .args(["worktree", "add", "--quiet", "--detach"]) + .arg(&worktree) + .arg("HEAD") + .output() + .map_err(|error| format!("git worktree add: {error}"))?; + if !worktree_output.status.success() { + return Err(format!( + "git worktree add refused: {}", + String::from_utf8_lossy(&worktree_output.stderr).trim() + )); + } + let source_facts = facts(&fixture.snapshot()?)?; + let clone_facts = facts(&RepositorySnapshot::read(&clone)?)?; + let worktree_snapshot = RepositorySnapshot::read(&worktree)?; + let worktree_facts = facts(&worktree_snapshot)?; + assert_eq!(source_facts, clone_facts); + assert_eq!(source_facts, worktree_facts); + assert!(worktree_snapshot.files().get(GIT_STORAGE).is_none()); + let _removed = Command::new("git") + .current_dir(&fixture.root) + .args(["worktree", "remove", "--force"]) + .arg(&worktree) + .output(); + let _clone_removed = fs::remove_dir_all(&clone); Ok(()) } - /// A file the tree does not carry is ABSENT, and absent is not empty. - /// - /// Planted reversal for every fallback this model deleted. The reading used - /// to answer a missing manifest with an empty string, and an empty manifest - /// declares no prohibited edge — a law reporting clean about bytes nobody - /// opened. + /// Missing committed paths remain declared absent rather than empty. #[test] - fn a_path_the_tree_does_not_carry_is_absent_rather_than_empty() -> Result<(), String> { - let snapshot = repository_snapshot()?; + fn a_missing_committed_path_is_absent() -> Result<(), String> { + let fixture = GitFixture::named("absent")?; + fixture.write("present.txt", b"present\n")?; + fixture.commit()?; + let snapshot = fixture.snapshot()?; assert!(matches!( - snapshot.files().text("no/such/file.md"), + snapshot.files().text("missing.txt"), Read::DeclaredAbsent(_) )); - assert!( - snapshot - .files() - .text("Cargo.toml") - .required("the root manifest")? - .contains("[workspace]"), - "the root manifest was not read" + Ok(()) + } + + /// The NUL roster keeps a newline-bearing path inside one record. + #[test] + fn tracked_population_is_nul_delimited() -> Result<(), String> { + let parsed = parse_tracked_blobs(b"100644 blob abcdef\tline\nname.txt\0")?; + assert_eq!(parsed.len(), 1); + assert_eq!( + parsed.first().map(|entry| entry.path.as_str()), + Some("line\nname.txt") ); Ok(()) } + + /// A line-terminated roster cannot impersonate Git's NUL contract. + #[test] + fn an_unterminated_tracked_record_refuses() { + let found = parse_tracked_blobs(b"100644 blob abcdef\tpath.txt\n"); + assert!( + found.is_err_and(|refusal| refusal.contains("final path record with NUL")), + "a non-NUL Git population was accepted" + ); + } } diff --git a/xtask/src/repository/types.rs b/xtask/src/repository/types.rs index 43f57ea..c597a34 100644 --- a/xtask/src/repository/types.rs +++ b/xtask/src/repository/types.rs @@ -116,8 +116,6 @@ pub(crate) enum AbsenceReason { /// The root declares no `Cargo.toml`, so there is no workspace for cargo to /// read and no reading to ask about. NotAWorkspaceCheckout, - /// The root is not a git checkout, so no commit names what was read. - NotAGitCheckout, /// The document declares no data block carrying the schema asked for. NoBlockDeclaresThisSchema, /// The document is there and states no such key. @@ -131,7 +129,6 @@ impl fmt::Display for AbsenceReason { AbsenceReason::NotAWorkspaceCheckout => { "the root declares no Cargo.toml, so cargo reports nothing here" } - AbsenceReason::NotAGitCheckout => "the root is not a git checkout", AbsenceReason::NoBlockDeclaresThisSchema => { "no fenced data block in that document declares this schema" } From bd0f00b96600d8f5d6add26c1ce2ed2bba69301b Mon Sep 17 00:00:00 2001 From: Heyoub Date: Sat, 15 Aug 2026 13:56:54 -0400 Subject: [PATCH 2/5] Strengthen B2 repository binding --- xtask/src/checks/coupling.rs | 146 +++++++++- xtask/src/repository/snapshot.rs | 447 ++++++++++++++++++++++++++++--- 2 files changed, 546 insertions(+), 47 deletions(-) diff --git a/xtask/src/checks/coupling.rs b/xtask/src/checks/coupling.rs index 5a1789b..9c2d87b 100644 --- a/xtask/src/checks/coupling.rs +++ b/xtask/src/checks/coupling.rs @@ -360,13 +360,16 @@ fn read_inline_module( /// Every name one module re-exports out of the named child module. /// -/// A glob contributes nothing: what stands behind one is a set this reader would -/// have to resolve, and a body left in the inner scope produces a refusal -/// somebody can argue with rather than a silence nobody can see. +/// Only a relative path to the immediate child contributes names. A leading +/// `::` is absolute rather than local. A glob, rename, or deeper path contributes +/// nothing: each carries facts this reader would have to resolve, and a family +/// left without a body produces a refusal somebody can argue with rather than a +/// false coupling nobody can see. fn reexported_from(child: &syn::Ident, items: &[syn::Item]) -> Vec { let mut names = Vec::new(); for item in items { if let syn::Item::Use(declared) = item + && declared.leading_colon.is_none() && let Some(tree) = immediate_child_reexport(&declared.tree, child) { reexported_names(tree, &mut names); @@ -400,19 +403,22 @@ fn immediate_child_reexport<'tree>( (rooted.ident == *child).then_some(&rooted.tree) } -/// Every name one `use` tree brings in, under the spelling the enclosing module -/// then knows it by. +/// Every direct name one selected immediate-child tree brings in. +/// +/// A group is only authoring shorthand for its direct `Name` members. Nested +/// paths, nested groups, renames, and globs require facts beyond one name in one +/// known child scope, so each stays unlifted. fn reexported_names(tree: &syn::UseTree, into: &mut Vec) { match *tree { syn::UseTree::Name(ref named) => into.push(named.ident.to_string()), - syn::UseTree::Rename(ref renamed) => into.push(renamed.rename.to_string()), syn::UseTree::Group(ref group) => { for inner in &group.items { - reexported_names(inner, into); + if let syn::UseTree::Name(named) = inner { + into.push(named.ident.to_string()); + } } } - syn::UseTree::Path(ref deeper) => reexported_names(&deeper.tree, into), - syn::UseTree::Glob(_) => {} + syn::UseTree::Path(_) | syn::UseTree::Rename(_) | syn::UseTree::Glob(_) => {} } } @@ -643,6 +649,128 @@ mod tests { assert!(verdict.offenders.is_empty(), "{:?}", verdict.offenders); } + /// Direct names in one group are still immediate-child re-exports. The + /// group changes only how several direct names are written, not which scope + /// declares either body. + #[test] + fn a_group_of_direct_names_resolves_in_the_publishing_module() { + let verdict = coupled_body_verdict(&source( + "pub use seat::{FirstRefusal, SecondRefusal};\n\ + \n\ + mod seat {\n\ + \x20 pub struct FirstRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + \x20 pub struct SecondRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + }\n\ + impl RefusalFamily for FirstRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n\ + impl RefusalFamily for SecondRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 2); + assert_eq!(verdict.coupled, 2, "{:?}", verdict.offenders); + assert!(verdict.offenders.is_empty(), "{:?}", verdict.offenders); + } + + /// Planted reversal: a nested path publishes the nested body's identity, + /// not an identically named top-level body inside the immediate child. This + /// bounded reader does not resolve the deeper path, so it must leave the + /// outer family loudly bodyless rather than couple the innocent body. + #[test] + fn a_nested_re_export_cannot_lift_a_top_level_collision() { + let verdict = coupled_body_verdict(&source( + "pub use seat::nested::DemoRefusal;\n\ + mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + \x20 mod nested {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 issues: NonEmptyBounded,\n\ + \x20 }\n\ + \x20 }\n\ + }\n\ + impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 0, "{:?}", verdict.offenders); + assert!( + verdict + .offenders + .first() + .is_some_and(|offence| offence.contains("no `pub struct DemoRefusal`")), + "{:?}", + verdict.offenders + ); + } + + /// Planted reversal: a rename carries both a source and a published name. + /// This reader carries only one body identity, so it refuses to guess by + /// leaving the outer family bodyless rather than lifting a top-level body + /// that merely matches the alias. + #[test] + fn a_renamed_re_export_cannot_lift_an_alias_collision() { + let verdict = coupled_body_verdict(&source( + "pub use seat::OriginalRefusal as DemoRefusal;\n\ + mod seat {\n\ + \x20 pub struct OriginalRefusal {\n\ + \x20 issues: NonEmptyBounded,\n\ + \x20 }\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + }\n\ + impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 0, "{:?}", verdict.offenders); + assert!( + verdict + .offenders + .first() + .is_some_and(|offence| offence.contains("no `pub struct DemoRefusal`")), + "{:?}", + verdict.offenders + ); + } + + /// Planted reversal: a leading `::` makes the path absolute. It is not the + /// immediate child declared in this module even when the terminal spelling + /// matches, so no local body is lifted through it. + #[test] + fn an_absolute_re_export_does_not_lift_a_local_child() { + let verdict = coupled_body_verdict(&source( + "pub use ::seat::DemoRefusal;\n\ + mod seat {\n\ + \x20 pub struct DemoRefusal {\n\ + \x20 body: AdmittedPrefix,\n\ + \x20 }\n\ + }\n\ + impl RefusalFamily for DemoRefusal {\n\ + \x20 const SHAPE: FamilyShape = FamilyShape::IssueCollection;\n\ + }\n", + )); + assert_eq!(verdict.declared, 1); + assert_eq!(verdict.coupled, 0, "{:?}", verdict.offenders); + assert!( + verdict + .offenders + .first() + .is_some_and(|offence| offence.contains("no `pub struct DemoRefusal`")), + "{:?}", + verdict.offenders + ); + } + /// A different prefix is not normalized into the immediate child. Doing so /// would be partial name resolution and could lift a body from another /// scope merely because its terminal spelling matched. diff --git a/xtask/src/repository/snapshot.rs b/xtask/src/repository/snapshot.rs index fb805e2..5ccc4bf 100644 --- a/xtask/src/repository/snapshot.rs +++ b/xtask/src/repository/snapshot.rs @@ -31,6 +31,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; use std::ffi::OsString; use std::fmt; +use std::fs; use std::io::{BufRead, BufReader, Read as IoRead, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -43,6 +44,42 @@ use crate::repository::types::{AbsenceReason, CanonicalPath, LinkState, Read, Re /// Git's own storage coordinate at a checkout root. const GIT_STORAGE: &str = ".git"; +/// Git-specific ambient inputs that can redirect repository identity, refs, +/// objects, index, worktree, or configuration. +/// +/// Git's own `rev-parse --local-env-vars` roster supplies the repository-local +/// core. The additional namespace, config-file, quarantine, and discovery +/// variables are documented Git routing inputs that can change the same +/// answers. Numbered `GIT_CONFIG_KEY_*` and `GIT_CONFIG_VALUE_*` entries are +/// inert once `GIT_CONFIG_COUNT` is absent, so no unbounded environment scan is +/// needed. `GIT_NO_REPLACE_OBJECTS` is deliberately not listed: [`git`] sets it +/// to the one admitted value after clearing this roster. +const GIT_ROUTING_ENVIRONMENT: &[&str] = &[ + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CEILING_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_CONFIG", + "GIT_CONFIG_COUNT", + "GIT_CONFIG_GLOBAL", + "GIT_CONFIG_NOSYSTEM", + "GIT_CONFIG_PARAMETERS", + "GIT_CONFIG_SYSTEM", + "GIT_DIR", + "GIT_DISCOVERY_ACROSS_FILESYSTEM", + "GIT_GRAFT_FILE", + "GIT_IMPLICIT_WORK_TREE", + "GIT_INDEX_FILE", + "GIT_INTERNAL_SUPER_PREFIX", + "GIT_NAMESPACE", + "GIT_OBJECT_DIRECTORY", + "GIT_PREFIX", + "GIT_QUARANTINE_PATH", + "GIT_REPLACE_REF_BASE", + "GIT_SHALLOW_FILE", + "GIT_SUPER_PREFIX", + "GIT_WORK_TREE", +]; + /// The metaprogramming subsystem's directory. /// /// Four laws name it, so the name answers to no single law and stands here @@ -80,13 +117,20 @@ impl RepositorySnapshot { /// /// Git owns both membership and bytes: `ls-tree -z` derives the exact blob /// population and `cat-file --batch` reads those immutable objects. Ignored - /// and untracked filesystem entries therefore cannot enter this type. A - /// tracked checkout difference or a moving `HEAD` refuses construction. + /// and untracked filesystem entries therefore cannot enter this type. Every + /// committed regular file is compared directly with the type and bytes at + /// the explicit root; a mismatch or moving `HEAD` refuses construction. + /// Executable mode is not part of this byte-binding claim. /// - /// Cargo metadata remains a live observation. The committed state and - /// tracked-clean posture are checked again after that process so a run does - /// not join a stable committed projection to an observation taken while the - /// checkout moved. + /// A committed symbolic-link entry remains a raw Git fact rather than being + /// followed or silently treated as a regular file. The existing + /// `lf-and-no-symlinks` repository law consumes that mode and refuses it, so + /// no accepted qualification can spend a symlink-bearing snapshot. + /// + /// Cargo metadata remains a live observation. The committed state and every + /// regular file's direct live-byte match are checked again after that + /// process so a run does not join a stable committed projection to an + /// observation taken while the checkout moved. pub(crate) fn read(root: &Path) -> Result { Self::read_with_after_files(root, || Ok(())) } @@ -111,17 +155,16 @@ impl RepositorySnapshot { after_files: impl FnOnce() -> Result<(), String>, ) -> Result { let before = committed_tree_with_after_commit(root, after_initial_commit)?; - require_tracked_clean(root)?; let files = CanonicalFileMap::read(root, &before.tree)?; after_files()?; require_same_committed_state(&before, &committed_tree(root)?)?; - require_tracked_clean(root)?; + require_live_regular_file_bytes(root, &files)?; let cargo = CargoSnapshot::read(&files); let rust = RustSyntaxSnapshot::read(&files); let markdown = MarkdownSnapshot::read(&files); let cargo_observation = CargoObservation::read(root, &files); require_same_committed_state(&before, &committed_tree(root)?)?; - require_tracked_clean(root)?; + require_live_regular_file_bytes(root, &files)?; Ok(Self { files, cargo, @@ -340,12 +383,7 @@ fn parse_tracked_blobs(output: &[u8]) -> Result, String> { String::from_utf8_lossy(raw_path) ) })?; - if spelled.contains('\\') { - return Err(format!( - "git tracks `{spelled}` with a literal backslash; canonical repository paths use \ - forward slashes, so replacing that byte would collapse two identities" - )); - } + validate_canonical_relative_path(spelled)?; let path = CanonicalPath::spelled(spelled); if !seen.insert(path.clone()) { return Err(format!("git listed committed path `{path}` more than once")); @@ -359,6 +397,34 @@ fn parse_tracked_blobs(output: &[u8]) -> Result, String> { Ok(tracked) } +/// Requires a Git-derived path to be one safe canonical descendant. +/// +/// This is checked before the spelling becomes a [`CanonicalPath`] and checked +/// again immediately before the only `root.join`. Git normally emits this +/// grammar already; refusing it here keeps a malformed or substituted Git +/// response from turning repository identity into filesystem traversal. +fn validate_canonical_relative_path(spelled: &str) -> Result<(), String> { + let bytes = spelled.as_bytes(); + let windows_prefix = matches!(bytes, [drive, b':', ..] if drive.is_ascii_alphabetic()); + let unsafe_spelling = spelled.is_empty() + || spelled.starts_with('/') + || spelled.ends_with('/') + || spelled.contains("//") + || spelled.contains('\\') + || windows_prefix + || spelled + .split('/') + .any(|component| component == "." || component == ".."); + if unsafe_spelling || Path::new(spelled).is_absolute() { + Err(format!( + "git reported unsafe non-canonical repository path `{spelled}`; committed paths must \ + be non-empty relative descendants with ordinary components" + )) + } else { + Ok(()) + } +} + /// Reads every committed blob through one batch process. fn read_tracked_blobs( root: &Path, @@ -519,30 +585,71 @@ fn committed_tree_with_after_commit( Ok(CommittedTree { commit, tree }) } -/// Requires every tracked checkout path to match `HEAD`. +/// Requires every committed regular file to exist as a regular file with the +/// same bytes beneath the explicit root. /// -/// Untracked and ignored paths are deliberately excluded rather than refused: -/// membership and bytes come from the committed tree, so neither population can -/// enter the snapshot. The `-z` porcelain contract is used so a tracked path -/// containing a newline cannot hide by changing record boundaries. -fn require_tracked_clean(root: &Path) -> Result<(), String> { - let output = git(root) - .args(["status", "--porcelain=v2", "-z", "--untracked-files=no"]) - .stderr(Stdio::piped()) - .output() - .map_err(|error| format!("git status --porcelain=v2: {error}"))?; - if !output.status.success() { - return Err(format!( - "git could not establish tracked checkout cleanliness: {}", - String::from_utf8_lossy(&output.stderr).trim() - )); +/// This does not ask Git whether the worktree is clean. Index flags, status +/// refresh policy, filesystem monitors, and `core.worktree` therefore cannot +/// hide a mismatch. Ignored and untracked paths remain outside the committed +/// population. Executable mode is an explicit nonclaim: the binding is to bytes +/// and file kind. An index-only staged difference with unchanged live bytes is +/// likewise outside this snapshot binding; qualification's final worktree-clean +/// stage owns that repository-state claim. +/// +/// A committed symlink is deliberately not opened or followed here. Its Git +/// mode stays in [`FileFact`] for the existing `lf-and-no-symlinks` law, whose +/// refusal means no accepted qualification can spend that unbound live entry. +fn require_live_regular_file_bytes(root: &Path, files: &CanonicalFileMap) -> Result<(), String> { + for (path, fact) in files.iter() { + match *fact.link().required(path.as_str())? { + LinkState::RegularFile => require_live_regular_file(root, path, fact)?, + LinkState::Symlink => { + // The raw Git mode is the owner fact. Following the live link + // here would turn its target into repository bytes; treating it + // as regular would erase the exact fact the no-symlink law owns. + } + } + } + Ok(()) +} + +/// Compares one committed regular file with the explicit root twice around the +/// byte read, refusing missing, unreadable, symlink, directory, or changed +/// live state with the exact canonical path. +fn require_live_regular_file( + root: &Path, + path: &CanonicalPath, + fact: &FileFact, +) -> Result<(), String> { + validate_canonical_relative_path(path.as_str())?; + let live_path = root.join(path.as_str()); + require_live_regular_kind(&live_path, path)?; + let live = fs::read(&live_path).map_err(|error| { + format!("committed regular file `{path}` could not be read at the explicit root: {error}") + })?; + require_live_regular_kind(&live_path, path)?; + let committed = fact.bytes().required(path.as_str())?; + if live == *committed { + Ok(()) + } else { + Err(format!( + "committed regular file `{path}` differs from the bytes at the explicit root" + )) } - if output.stdout.is_empty() { +} + +/// Requires one live path to be an ordinary file without following a symlink. +fn require_live_regular_kind(live_path: &Path, path: &CanonicalPath) -> Result<(), String> { + let metadata = fs::symlink_metadata(live_path).map_err(|error| { + format!( + "committed regular file `{path}` is missing or unreadable at the explicit root: {error}" + ) + })?; + if metadata.file_type().is_file() { Ok(()) } else { - Err(String::from( - "tracked checkout bytes differ from HEAD; a committed snapshot refuses rather than \ - report about different live bytes beside the committed tree", + Err(format!( + "committed regular file `{path}` is not a regular file at the explicit root" )) } } @@ -580,10 +687,19 @@ fn revision(root: &Path, spelling: &str) -> Result { Ok(reported.trim().to_owned()) } -/// One Git command with replacement-object rewriting disabled. +/// One Git command bound to the explicit worktree coordinate. +/// +/// Environment routing is removed, repository-local `core.worktree` is +/// overridden by the command-line coordinate, and replacement-object rewriting +/// is disabled. Git still discovers the matching `.git` directory or gitfile at +/// `root`, which preserves ordinary clones and linked worktrees alike. fn git(root: &Path) -> Command { let mut command = Command::new("git"); - command.current_dir(root).env("GIT_NO_REPLACE_OBJECTS", "1"); + command.current_dir(root).arg("--work-tree").arg(root); + for variable in GIT_ROUTING_ENVIRONMENT { + command.env_remove(variable); + } + command.env("GIT_NO_REPLACE_OBJECTS", "1"); command } @@ -760,6 +876,26 @@ mod tests { run_git(&self.root, arguments) } + /// The unpinned porcelain answer used only to prove a hostile really is + /// hidden from Git status before direct byte comparison rejects it. + fn unpinned_tracked_status(&self) -> Result, String> { + let output = Command::new("git") + .current_dir(&self.root) + .env("GIT_NO_REPLACE_OBJECTS", "1") + .args(["status", "--porcelain=v2", "-z", "--untracked-files=no"]) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("unpinned fixture status: {error}"))?; + if output.status.success() { + Ok(output.stdout) + } else { + Err(format!( + "unpinned fixture status refused: {}", + String::from_utf8_lossy(&output.stderr).trim() + )) + } + } + /// Reads the committed snapshot. fn snapshot(&self) -> Result { RepositorySnapshot::read(&self.root) @@ -916,6 +1052,128 @@ mod tests { Ok(()) } + /// Aggregate hostile: Git routing inherited from the process names another + /// repository, worktree, object directory, and index, while command-scope + /// config attempts to redirect `core.worktree` as well. The explicit root + /// remains the only repository identity the snapshot consumes. + /// + /// The hostile runs in a child test process so no global environment is + /// mutated under concurrently executing Rust tests. + #[test] + fn ambient_git_routing_cannot_redirect_an_explicit_root() -> Result<(), String> { + const CHILD_ROOT: &str = "THREADPAK_GIT_ROUTING_CHILD_ROOT"; + const EXPECTED_STATE: &str = "THREADPAK_GIT_ROUTING_EXPECTED_STATE"; + + if let Some(root) = std::env::var_os(CHILD_ROOT) { + let expected = std::env::var(EXPECTED_STATE) + .map_err(|error| format!("routing hostile expected state: {error}"))?; + let snapshot = RepositorySnapshot::read(Path::new(&root))?; + assert_eq!(snapshot.committed().to_string(), expected); + assert!(snapshot.files().get("explicit.txt").is_some()); + assert!(snapshot.files().get("alternate.txt").is_none()); + return Ok(()); + } + + let explicit = GitFixture::named("routing-explicit")?; + explicit.write("explicit.txt", b"explicit repository\n")?; + explicit.commit()?; + let expected = super::committed_tree(&explicit.root)?.to_string(); + + let alternate = GitFixture::named("routing-alternate")?; + alternate.write("alternate.txt", b"alternate repository\n")?; + alternate.commit()?; + let alternate_git = alternate.root.join(GIT_STORAGE); + + let child = std::env::current_exe() + .map_err(|error| format!("current xtask test executable: {error}"))?; + let output = Command::new(child) + .arg("ambient_git_routing_cannot_redirect_an_explicit_root") + .arg("--test-threads=1") + .env(CHILD_ROOT, &explicit.root) + .env(EXPECTED_STATE, expected) + .env("GIT_DIR", &alternate_git) + .env("GIT_WORK_TREE", &alternate.root) + .env("GIT_COMMON_DIR", &alternate_git) + .env("GIT_INDEX_FILE", alternate_git.join("index")) + .env("GIT_OBJECT_DIRECTORY", alternate_git.join("objects")) + .env("GIT_CONFIG_COUNT", "1") + .env("GIT_CONFIG_KEY_0", "core.worktree") + .env("GIT_CONFIG_VALUE_0", &alternate.root) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("routing-hostile child test: {error}"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + if !output.status.success() || !stdout.contains("1 passed") { + return Err(format!( + "routing-hostile child did not establish the explicit root:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) + } + + /// Aggregate hostile: repository-local `core.worktree` points at a clean + /// alternate checkout while the explicit root's tracked bytes are dirty. + /// Unpinned porcelain demonstrates the concealment; the snapshot's central + /// command boundary pins the named root and refuses those dirty bytes. + #[test] + fn local_core_worktree_cannot_redirect_the_cleanliness_guard() -> Result<(), String> { + let explicit = GitFixture::named("configured-worktree-explicit")?; + explicit.write("tracked.txt", b"committed\n")?; + explicit.commit()?; + + let alternate = GitFixture::named("configured-worktree-alternate")?; + let alternate_spelling = alternate + .root + .to_str() + .ok_or_else(|| String::from("alternate worktree has no Unicode spelling"))?; + explicit.git(&[ + "--work-tree", + alternate_spelling, + "checkout", + "--force", + "HEAD", + "--", + ".", + ])?; + explicit.git(&["config", "core.worktree", alternate_spelling])?; + let pinned = super::git(&explicit.root) + .args(["rev-parse", "--show-toplevel"]) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("pinned configured-worktree root: {error}"))?; + if !pinned.status.success() { + return Err(format!( + "pinned configured-worktree root refused: {}", + String::from_utf8_lossy(&pinned.stderr).trim() + )); + } + let pinned_root = std::str::from_utf8(&pinned.stdout) + .map_err(|error| format!("pinned Git root is not Unicode: {error}"))? + .trim(); + assert_eq!( + fs::canonicalize(pinned_root) + .map_err(|error| format!("canonical pinned Git root: {error}"))?, + fs::canonicalize(&explicit.root) + .map_err(|error| format!("canonical explicit fixture root: {error}"))? + ); + explicit.write("tracked.txt", b"dirty explicit bytes\n")?; + + assert!( + explicit.unpinned_tracked_status()?.is_empty(), + "the hostile did not conceal the explicit-root difference" + ); + + let found = explicit.snapshot(); + assert!( + found.is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("differs from the bytes")), + "repository-local core.worktree redirected the committed cleanliness guard" + ); + Ok(()) + } + /// A tracked checkout difference refuses the committed constructor. #[test] fn tracked_dirt_refuses_the_committed_snapshot() -> Result<(), String> { @@ -925,12 +1183,100 @@ mod tests { fixture.write("tracked.txt", b"different\n")?; let found = fixture.snapshot(); assert!( - found.is_err_and(|refusal| refusal.contains("tracked checkout bytes differ")), + found.is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("differs from the bytes")), "tracked dirt entered a committed snapshot" ); Ok(()) } + /// A committed regular file missing from the explicit root is unknown, not + /// an empty or absent contribution. + #[test] + fn a_missing_committed_regular_file_refuses() -> Result<(), String> { + let fixture = GitFixture::named("missing-regular")?; + fixture.write("tracked.txt", b"committed\n")?; + fixture.commit()?; + fs::remove_file(fixture.root.join("tracked.txt")) + .map_err(|error| format!("remove committed fixture file: {error}"))?; + let found = fixture.snapshot(); + assert!( + found.is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("missing or unreadable")), + "a missing committed regular file entered the byte binding" + ); + Ok(()) + } + + /// A directory at a committed regular-file path is a type mismatch, even + /// if Git status or another reader would describe only the path spelling. + #[test] + fn a_directory_cannot_replace_a_committed_regular_file() -> Result<(), String> { + let fixture = GitFixture::named("regular-became-directory")?; + fixture.write("tracked.txt", b"committed\n")?; + fixture.commit()?; + fs::remove_file(fixture.root.join("tracked.txt")) + .map_err(|error| format!("remove committed fixture file: {error}"))?; + fs::create_dir(fixture.root.join("tracked.txt")) + .map_err(|error| format!("replace committed fixture file with directory: {error}"))?; + let found = fixture.snapshot(); + assert!( + found + .is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("not a regular file")), + "a directory impersonated a committed regular file" + ); + Ok(()) + } + + /// Planted reversal: `assume-unchanged` makes porcelain status omit a + /// modified tracked path. Direct comparison ignores that index hint and + /// refuses the bytes themselves. + #[test] + fn assume_unchanged_cannot_hide_tracked_dirt() -> Result<(), String> { + let fixture = GitFixture::named("assume-unchanged")?; + fixture.write("tracked.txt", b"committed\n")?; + fixture.commit()?; + fixture.git(&["update-index", "--assume-unchanged", "tracked.txt"])?; + fixture.write("tracked.txt", b"hidden difference\n")?; + assert!( + fixture.unpinned_tracked_status()?.is_empty(), + "the assume-unchanged hostile did not hide from porcelain" + ); + + let found = fixture.snapshot(); + assert!( + found.is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("differs from the bytes")), + "assume-unchanged hid tracked dirt from the committed guard" + ); + Ok(()) + } + + /// Planted reversal: `skip-worktree` creates the other status-blind index + /// posture. Direct comparison again ignores the hint and reads the explicit + /// path. + #[test] + fn skip_worktree_cannot_hide_tracked_dirt() -> Result<(), String> { + let fixture = GitFixture::named("skip-worktree")?; + fixture.write("tracked.txt", b"committed\n")?; + fixture.commit()?; + fixture.git(&["update-index", "--skip-worktree", "tracked.txt"])?; + fixture.write("tracked.txt", b"hidden difference\n")?; + assert!( + fixture.unpinned_tracked_status()?.is_empty(), + "the skip-worktree hostile did not hide from porcelain" + ); + + let found = fixture.snapshot(); + assert!( + found.is_err_and(|refusal| refusal.contains("tracked.txt") + && refusal.contains("differs from the bytes")), + "skip-worktree hid tracked dirt from the committed guard" + ); + Ok(()) + } + /// Root and nested ignored bytes are outside the committed population. #[test] fn ignored_files_never_enter_the_committed_snapshot() -> Result<(), String> { @@ -970,10 +1316,35 @@ mod tests { ); } + /// Git-derived paths must be safe canonical descendants before any + /// filesystem join. Traversal, absolute/prefixed, ambiguous-separator, and + /// empty spellings all refuse at the population reader. + #[test] + fn unsafe_git_paths_refuse_before_live_join() { + for path in [ + "", + "/absolute.txt", + "C:/prefixed.txt", + "trailing/", + "repeated//separator.txt", + "./dot.txt", + "nested/../escape.txt", + "back\\slash.txt", + ] { + let record = format!("100644 blob abcdef\t{path}\0"); + assert!( + parse_tracked_blobs(record.as_bytes()) + .is_err_and(|refusal| refusal.contains("unsafe non-canonical")), + "unsafe Git path `{path}` entered the canonical population" + ); + } + } + /// A clone and linked worktree of one tree have one canonical population. #[test] fn clone_and_linked_worktree_read_identically() -> Result<(), String> { let fixture = GitFixture::named("checkout-shapes")?; + fixture.write(".gitattributes", b"* text=auto eol=lf\n")?; fixture.write("a.txt", b"a\n")?; fixture.write("nested/b.txt", b"b\n")?; fixture.commit()?; From 724a8d74cbbc000d0d6614f8a726af850835cfc2 Mon Sep 17 00:00:00 2001 From: Heyoub Date: Sat, 15 Aug 2026 19:51:44 -0400 Subject: [PATCH 3/5] Preserve scope guard visibility coordinates --- src/02_identity/README.md | 38 + src/02_identity/mod.rs | 193 ++++- testpak/README.md | 6 + ...ion-scope-guard-cannot-be-laundered.stderr | 6 +- .../a-scope-guard-alias-cannot-widen-reach.rs | 89 +++ ...cope-guard-alias-cannot-widen-reach.stderr | 282 +++++++ ...scope-guard-reexport-cannot-widen-reach.rs | 155 ++++ ...e-guard-reexport-cannot-widen-reach.stderr | 736 ++++++++++++++++++ ...cope-guard-signature-cannot-widen-reach.rs | 89 +++ ...-guard-signature-cannot-widen-reach.stderr | 282 +++++++ ...-representation-cannot-be-laundered.stderr | 6 +- ...me-comparison-on-a-production-guard.stderr | 10 +- ...scope-comparison-on-a-stamped-guard.stderr | 2 +- testpak/tests/scope_guard_alias_visibility.rs | 148 ++++ .../tests/scope_guard_signature_visibility.rs | 148 ++++ testpak/tests/scope_guard_visibility.rs | 357 +++++++++ xtask/fixtures/macro-consumer/src/lib.rs | 457 ++++++++++- 17 files changed, 2978 insertions(+), 26 deletions(-) create mode 100644 testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs create mode 100644 testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.stderr create mode 100644 testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs create mode 100644 testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.stderr create mode 100644 testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs create mode 100644 testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.stderr create mode 100644 testpak/tests/scope_guard_alias_visibility.rs create mode 100644 testpak/tests/scope_guard_signature_visibility.rs create mode 100644 testpak/tests/scope_guard_visibility.rs diff --git a/src/02_identity/README.md b/src/02_identity/README.md index ff949dd..990b7ac 100644 --- a/src/02_identity/README.md +++ b/src/02_identity/README.md @@ -100,6 +100,32 @@ trips `non_snake_case`, which the lint wall denies — no attribute suppresses anything, and two stamps naming one module in one file collide as a duplicate definition. +The caller-coordinate re-export is the guard's canonical exported spelling. +The caller's visibility appears there exactly once. The front grammar also +transports the same reach one module into the private generated child: private +and `self` become `super`, `super` gains one `super` segment, and absolute paths +and `pub` keep their coordinate-independent meaning. The type and both methods +carry that transported reach. Code already inside the authorized scope may name +the generated child path, but no generated item is broad enough for a wider +same-coordinate re-export, type alias, or public signature. + +The admitted direct-token population is Rust's private and public forms, +`pub(self)`, `pub(super)`, `pub(crate)`, the equivalent `pub(in self)`, +`pub(in super)`, and `pub(in crate)` forms, relative `super` chains, absolute +`crate` paths, and an outer macro's coordinate-invariant `$crate` path. A whole +visibility forwarded as a captured `$vis:vis` is opaque: the stamp refuses it +instead of treating an unknown reach as crate-wide. + +Visibility selection does not duplicate the guard. One transcriber owns the +type, private seat, and both methods; the front arms choose only the transported +internal reach and the caller-coordinate re-export. +The `@transcribe` arm follows the root register stamp's internal-arm precedent: +Rust exports the arm, so a direct invocation is hand-authored authority over a +new guard's two visibilities and is outside the front grammar's transport claim. +It still cannot change an existing guard because the module or item name +collides, and it cannot add a constructor or accessor because the shared body +still contains the same private field and complete method set. + The machine's production scope guards are stamped. Nine of them were tuple structs whose position field was `pub`, which is both a public constructor and a public accessor — so the road out the stamp refuses to emit was standing open @@ -168,6 +194,18 @@ obligations: challenge_kind: compile-law green: laws.rs identity::a_stamped_scope_guard_matches_its_hand_written_twin red: testpak/tests/compile-fail/cross-scope-comparison-on-a-stamped-guard.rs + - id: identity.scope-guard-visibility-is-caller-relative + challenge_kind: compile-refusal + green: testpak/tests/scope_guard_visibility.rs + red: testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs + - id: identity.scope-guard-alias-cannot-widen + challenge_kind: compile-refusal + green: testpak/tests/scope_guard_alias_visibility.rs + red: testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs + - id: identity.scope-guard-signature-cannot-widen + challenge_kind: compile-refusal + green: testpak/tests/scope_guard_signature_visibility.rs + red: testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs - id: identity.admission-joins-creation-to-class challenge_kind: compile-refusal green: laws.rs identity::admission_joins_creation_to_class diff --git a/src/02_identity/mod.rs b/src/02_identity/mod.rs index 64ec7ee..3af00cf 100644 --- a/src/02_identity/mod.rs +++ b/src/02_identity/mod.rs @@ -98,13 +98,23 @@ pub use types::{ /// that glob unused, which is a denied warning at the call site rather than /// anything silent. /// -/// Both operations carry the caller's own `$vis`, so the road in and the -/// comparison are reachable exactly as far as the role they serve and never one -/// step further; the re-export carries it too, and it is the single gate on the -/// type. `$vis` must reach at least the invoking module: a guard stamped with no -/// visibility at all would be sealed inside a module nothing can name, so the -/// proof surface's demonstration guard is stamped `pub(crate)`, which is the -/// reach a bare private guard had before the seat moved. +/// The caller's visibility is emitted once on the canonical re-export, at the +/// coordinate where the caller wrote it. The front grammar separately moves +/// that reach one level into the generated child: private and `self` become +/// `super`, `super` gains one `super` segment, and absolute paths and `pub` +/// remain absolute. The type and both methods carry that transported reach. +/// Therefore a generated path can be named only from a scope already allowed +/// by the caller; a same-coordinate re-export, alias, or signature cannot +/// publish the guard farther. The child module itself stays private, so the +/// caller-coordinate re-export remains the canonical exported spelling. +/// +/// Direct source tokens cover Rust's private, `pub`, shorthand, relative +/// `pub(in super::...)`, and absolute `pub(in ...)` families. An absolute path +/// produced with an outer macro's `$crate` is preserved unchanged. A whole +/// visibility first captured as `$vis:vis` by another macro is opaque to +/// `macro_rules!`, so this stamp refuses it instead of guessing a reach; the +/// wrapper must author the literal front syntax or explicitly own an internal +/// transcriber call. /// /// # Where the stamp lives /// @@ -113,6 +123,31 @@ pub use types::{ /// about macro namespacing and is not a root admission of a semantic noun — the /// stamp declares no type of its own and owns no meaning. /// +/// The `@transcribe` rule is the stamp's shared implementation arm, following +/// the root `closed_register!` precedent; it is not the public front grammar. +/// Because `macro_rules!` exports all arms together, Rust does not make that +/// spelling private. A direct caller is hand-authoring both visibilities and +/// therefore owns their relationship; the front grammar's exact-transport +/// guarantee does not apply. The transcriber still owns one body with a private +/// tuple field and exactly two methods, and a direct call cannot alter an +/// existing guard because its generated module or item name collides. Rust also +/// refuses a requested re-export that exceeds the hand-authored internal item. +/// +/// A direct arm call that asks the re-export to exceed the internal reach is +/// refused by rustc: +/// +/// ```compile_fail +/// # #[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// # pub struct DemoScopeId; +/// threadpak::scope_guard_version! { +/// @transcribe +/// [pub(crate)] +/// [pub] +/// /// No public re-export can widen this crate-confined item. +/// struct TooWideVersion over DemoScopeId, seated in mod too_wide_version; +/// } +/// ``` +/// /// # The invocation /// /// ``` @@ -133,14 +168,140 @@ pub use types::{ macro_rules! scope_guard_version { ( $(#[$note:meta])* - $vis:vis struct $name:ident over $scope:ty, seated in mod $home:ident; + pub struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub] + [pub] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(crate) struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(crate)] + [pub(crate)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(self) struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(super)] + [pub(self)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(super) struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(in super::super)] + [pub(super)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(in self $(:: $relative:ident)*) struct $name:ident over $scope:ty, + seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(in super $(:: $relative)*)] + [pub(in self $(:: $relative)*)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(in super $(:: $relative:ident)*) struct $name:ident over $scope:ty, + seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(in super::super $(:: $relative)*)] + [pub(in super $(:: $relative)*)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(in crate $(:: $absolute:ident)*) struct $name:ident over $scope:ty, + seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(in crate $(:: $absolute)*)] + [pub(in crate $(:: $absolute)*)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + pub(in $absolute:path) struct $name:ident over $scope:ty, + seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(in $absolute)] + [pub(in $absolute)] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + $(#[$note:meta])* + struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @capture_private + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + @capture_private + $(#[$note:meta])* + $caller_vis:vis struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + $crate::scope_guard_version! { + @transcribe + [pub(super)] + [$caller_vis] + $(#[$note])* + struct $name over $scope, seated in mod $home; + } + }; + ( + @transcribe + [$internal_vis:vis] + [$caller_vis:vis] + $(#[$note:meta])* + struct $name:ident over $scope:ty, seated in mod $home:ident; ) => { mod $home { use super::*; $(#[$note])* #[derive(Debug, Clone, PartialEq, Eq, Hash)] - $vis struct $name($crate::identity::AuthorityPosition<$scope>); + $internal_vis struct $name($crate::identity::AuthorityPosition<$scope>); impl $name { /// The one road in: read one position under this role. @@ -151,7 +312,7 @@ macro_rules! scope_guard_version { /// that could leave this role could be re-entered under another /// one, and the role would have stopped being a wall. #[must_use] - $vis fn positioned( + $internal_vis fn positioned( position: $crate::identity::AuthorityPosition<$scope>, ) -> Self { Self(position) @@ -165,7 +326,7 @@ macro_rules! scope_guard_version { /// /// Returns the `OrderComparison` family body when the two /// positions do not share one scope. - $vis fn try_cmp_same_scope( + $internal_vis fn try_cmp_same_scope( &self, other: &Self, ) -> ::core::result::Result< @@ -177,6 +338,14 @@ macro_rules! scope_guard_version { } } - $vis use $home::$name; + $caller_vis use $home::$name; + }; + ( + $(#[$note:meta])* + $opaque_visibility:vis struct $name:ident over $scope:ty, seated in mod $home:ident; + ) => { + compile_error!( + "scope_guard_version! requires visibility tokens at its public front door; an opaque forwarded `vis` fragment cannot be transported one module deeper" + ); }; } diff --git a/testpak/README.md b/testpak/README.md index 8c812e4..5e720bc 100644 --- a/testpak/README.md +++ b/testpak/README.md @@ -76,6 +76,9 @@ discover it. | `tests/compile-fail/` | one fixture per discharged red twin | | `tests/independent_identity_transcript.rs` | the independent transcript lane: a second encoder, written from the published specification, re-deriving the services' own identities | | `tests/related_set_identity_levels.rs` | the layer above it: a second encoder re-deriving the CONTENT one mint site composes, and the postures a public reader can tell apart | +| `tests/scope_guard_alias_visibility.rs` | every narrow direct-token grammar class receives a lawful same-reach type alias at two invocation depths | +| `tests/scope_guard_signature_visibility.rs` | every narrow direct-token grammar class receives a lawful same-reach signature at two invocation depths | +| `tests/scope_guard_visibility.rs` | every direct-token grammar class and equivalent spelling keeps both methods and permits a same-reach re-export at two invocation depths | | `tests/stamp_row_ceiling.rs` | the closed-register stamp's row ceiling, spent to its last position through the public export — the positive control whose reversal is the fixture beside it | ## `Unreadable` is a failure class with its own alarm @@ -152,6 +155,9 @@ happened here, and both read as coverage until somebody compares the two. | `a-remainder-married-to-another-body.rs` | a completion belongs to the body it was minted with: the seats are private, so the cross-wired literal does not compile | | `a-rendering-taken-off-the-membership-only-draft.rs` | the frontage road is closed: the membership-only draft carries no rendering method at all | | `a-roster-past-the-stamp-ceiling.rs` | a declaration one row past the closed-register stamp's declared supply refuses with the STAMP'S OWN sentence, rather than dying against the compiler's recursion limit at a boundary nobody declared | +| `a-scope-guard-alias-cannot-widen-reach.rs` | at two invocation depths, every narrow direct-token grammar class independently reaches `private_interfaces` when a type alias exceeds the guard's transported reach | +| `a-scope-guard-reexport-cannot-widen-reach.rs` | at two invocation depths, every narrow direct-token grammar class independently refuses a same-coordinate wider re-export; direct uses cross each local and ancestor boundary, a public guard's generated child path stays private, and an opaque forwarded `vis` fragment refuses rather than being laundered through a fallback | +| `a-scope-guard-signature-cannot-widen-reach.rs` | at two invocation depths, every narrow direct-token grammar class independently reaches `private_interfaces` when a signature exceeds the guard's transported reach | | `a-services-refusal-body-reseated-by-literal.rs` | a services refusal family body cannot be written as a literal from outside the crate, so a body one seam established cannot be reseated under another seam's refusal | | `a-stamped-representation-cannot-be-laundered.rs` | two stamped guards over ONE scope: taking role A's position out and re-entering it under role B does not compile, in either direction | | `a-truncation-count-with-no-truncation-behind-it.rs` | the truncation posture's bound and count are private, so a posture describing a truncation that never happened is not a value a caller can write | diff --git a/testpak/tests/compile-fail/a-production-scope-guard-cannot-be-laundered.stderr b/testpak/tests/compile-fail/a-production-scope-guard-cannot-be-laundered.stderr index 88ede11..756a83d 100644 --- a/testpak/tests/compile-fail/a-production-scope-guard-cannot-be-laundered.stderr +++ b/testpak/tests/compile-fail/a-production-scope-guard-cannot-be-laundered.stderr @@ -7,8 +7,8 @@ error[E0423]: cannot initialize a tuple struct which contains private fields note: constructor is not visible here due to private fields --> $WORKSPACE/src/02_identity/mod.rs | - | $vis struct $name($crate::identity::AuthorityPosition<$scope>); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ private field + | $internal_vis struct $name($crate::identity::AuthorityPosition<$scope>); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ private field | ::: $WORKSPACE/src/11_navigation/types.rs | @@ -20,7 +20,7 @@ note: constructor is not visible here due to private fields | | pub struct FrameVersion over ReferenceFrameId, seated in mod frame_version; | | } | |_- in this macro invocation - = note: this error originates in the macro `crate::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `crate::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) help: you might have meant to use the `positioned` associated function | 53 | |position| threadpak::navigation::FrameVersion::positioned(position); diff --git a/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs b/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs new file mode 100644 index 0000000..6c6f970 --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs @@ -0,0 +1,89 @@ +//! Every narrow visibility arm refuses a wider same-coordinate type alias. + +#![deny(private_interfaces)] + +macro_rules! shallow_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +macro_rules! deep_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +pub mod shallow_ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + shallow_dollar_guard!(); + + pub(crate) type WiderPrivate = private_guard::PrivateGuard; + pub(crate) type WiderSelf = self_guard::SelfGuard; + pub(crate) type WiderInSelf = in_self_guard::InSelfGuard; + pub(crate) type WiderSuper = super_guard::SuperGuard; + pub(crate) type WiderInSuper = in_super_guard::InSuperGuard; + pub type WiderRelative = relative_guard::RelativeGuard; + pub type WiderCrate = crate_guard::CrateGuard; + pub type WiderInCrate = in_crate_guard::InCrateGuard; + pub(crate) type WiderAbsolute = absolute_guard::AbsoluteGuard; + pub(crate) type WiderDollar = dollar_guard::DollarGuard; + } +} + +pub mod deep { + pub mod ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + deep_dollar_guard!(); + + pub(crate) type WiderPrivate = private_guard::PrivateGuard; + pub(crate) type WiderSelf = self_guard::SelfGuard; + pub(crate) type WiderInSelf = in_self_guard::InSelfGuard; + pub(crate) type WiderSuper = super_guard::SuperGuard; + pub(crate) type WiderInSuper = in_super_guard::InSuperGuard; + pub(crate) type WiderRelative = relative_guard::RelativeGuard; + pub type WiderCrate = crate_guard::CrateGuard; + pub type WiderInCrate = in_crate_guard::InCrateGuard; + pub(crate) type WiderAbsolute = absolute_guard::AbsoluteGuard; + pub(crate) type WiderDollar = dollar_guard::DollarGuard; + } + } +} + +fn main() {} diff --git a/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.stderr b/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.stderr new file mode 100644 index 0000000..d3ce118 --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.stderr @@ -0,0 +1,282 @@ +error: type `shallow_ancestor::owner::private_guard::PrivateGuard` is more private than the item `shallow_ancestor::owner::WiderPrivate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:42:9 + | +42 | pub(crate) type WiderPrivate = private_guard::PrivateGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderPrivate` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::private_guard::PrivateGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:28:9 + | +28 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: the lint level is defined here + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:3:9 + | + 3 | #![deny(private_interfaces)] + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::self_guard::SelfGuard` is more private than the item `shallow_ancestor::owner::WiderSelf` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:43:9 + | +43 | pub(crate) type WiderSelf = self_guard::SelfGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderSelf` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::self_guard::SelfGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:29:9 + | +29 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_self_guard::InSelfGuard` is more private than the item `shallow_ancestor::owner::WiderInSelf` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:44:9 + | +44 | pub(crate) type WiderInSelf = in_self_guard::InSelfGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderInSelf` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::in_self_guard::InSelfGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:30:9 + | +30 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::super_guard::SuperGuard` is more private than the item `shallow_ancestor::owner::WiderSuper` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:45:9 + | +45 | pub(crate) type WiderSuper = super_guard::SuperGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderSuper` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::super_guard::SuperGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:31:9 + | +31 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_super_guard::InSuperGuard` is more private than the item `shallow_ancestor::owner::WiderInSuper` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:46:9 + | +46 | pub(crate) type WiderInSuper = in_super_guard::InSuperGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderInSuper` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::in_super_guard::InSuperGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:32:9 + | +32 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::relative_guard::RelativeGuard` is more private than the item `shallow_ancestor::owner::WiderRelative` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:47:9 + | +47 | pub type WiderRelative = relative_guard::RelativeGuard; + | ^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderRelative` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::relative_guard::RelativeGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:33:9 + | +33 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::crate_guard::CrateGuard` is more private than the item `shallow_ancestor::owner::WiderCrate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:48:9 + | +48 | pub type WiderCrate = crate_guard::CrateGuard; + | ^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderCrate` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::crate_guard::CrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:34:9 + | +34 | threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_crate_guard::InCrateGuard` is more private than the item `shallow_ancestor::owner::WiderInCrate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:49:9 + | +49 | pub type WiderInCrate = in_crate_guard::InCrateGuard; + | ^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderInCrate` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::in_crate_guard::InCrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:35:9 + | +35 | threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::absolute_guard::AbsoluteGuard` is more private than the item `shallow_ancestor::owner::WiderAbsolute` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:50:9 + | +50 | pub(crate) type WiderAbsolute = absolute_guard::AbsoluteGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderAbsolute` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::absolute_guard::AbsoluteGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:36:9 + | +36 | / threadpak::scope_guard_version! { +37 | | pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, +38 | | seated in mod absolute_guard; +39 | | } + | |_________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::dollar_guard::DollarGuard` is more private than the item `shallow_ancestor::owner::WiderDollar` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:51:9 + | +51 | pub(crate) type WiderDollar = dollar_guard::DollarGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `shallow_ancestor::owner::WiderDollar` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::dollar_guard::DollarGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:7:9 + | + 7 | / threadpak::scope_guard_version! { + 8 | | pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + 9 | | seated in mod dollar_guard; +10 | | } + | |_________^ +... +40 | shallow_dollar_guard!(); + | ----------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `shallow_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::private_guard::PrivateGuard` is more private than the item `ancestor::owner::WiderPrivate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:75:13 + | +75 | pub(crate) type WiderPrivate = private_guard::PrivateGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderPrivate` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::private_guard::PrivateGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:61:13 + | +61 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::self_guard::SelfGuard` is more private than the item `ancestor::owner::WiderSelf` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:76:13 + | +76 | pub(crate) type WiderSelf = self_guard::SelfGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderSelf` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::self_guard::SelfGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:62:13 + | +62 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_self_guard::InSelfGuard` is more private than the item `ancestor::owner::WiderInSelf` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:77:13 + | +77 | pub(crate) type WiderInSelf = in_self_guard::InSelfGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderInSelf` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::in_self_guard::InSelfGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:63:13 + | +63 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::super_guard::SuperGuard` is more private than the item `ancestor::owner::WiderSuper` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:78:13 + | +78 | pub(crate) type WiderSuper = super_guard::SuperGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderSuper` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::super_guard::SuperGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:64:13 + | +64 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_super_guard::InSuperGuard` is more private than the item `ancestor::owner::WiderInSuper` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:79:13 + | +79 | pub(crate) type WiderInSuper = in_super_guard::InSuperGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderInSuper` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::in_super_guard::InSuperGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:65:13 + | +65 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::relative_guard::RelativeGuard` is more private than the item `ancestor::owner::WiderRelative` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:80:13 + | +80 | pub(crate) type WiderRelative = relative_guard::RelativeGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderRelative` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::relative_guard::RelativeGuard` is only usable at visibility `pub(in crate::deep)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:66:13 + | +66 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::crate_guard::CrateGuard` is more private than the item `ancestor::owner::WiderCrate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:81:13 + | +81 | pub type WiderCrate = crate_guard::CrateGuard; + | ^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderCrate` is reachable at visibility `pub` + | +note: but type `ancestor::owner::crate_guard::CrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:67:13 + | +67 | threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_crate_guard::InCrateGuard` is more private than the item `ancestor::owner::WiderInCrate` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:82:13 + | +82 | pub type WiderInCrate = in_crate_guard::InCrateGuard; + | ^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderInCrate` is reachable at visibility `pub` + | +note: but type `ancestor::owner::in_crate_guard::InCrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:68:13 + | +68 | threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::absolute_guard::AbsoluteGuard` is more private than the item `ancestor::owner::WiderAbsolute` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:83:13 + | +83 | pub(crate) type WiderAbsolute = absolute_guard::AbsoluteGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderAbsolute` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::absolute_guard::AbsoluteGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:69:13 + | +69 | / threadpak::scope_guard_version! { +70 | | pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, +71 | | seated in mod absolute_guard; +72 | | } + | |_____________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::dollar_guard::DollarGuard` is more private than the item `ancestor::owner::WiderDollar` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:84:13 + | +84 | pub(crate) type WiderDollar = dollar_guard::DollarGuard; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ type alias `ancestor::owner::WiderDollar` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::dollar_guard::DollarGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-alias-cannot-widen-reach.rs:16:9 + | +16 | / threadpak::scope_guard_version! { +17 | | pub(in $crate::deep::ancestor) struct DollarGuard over Scope, +18 | | seated in mod dollar_guard; +19 | | } + | |_________^ +... +73 | deep_dollar_guard!(); + | -------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `deep_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs b/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs new file mode 100644 index 0000000..614de73 --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs @@ -0,0 +1,155 @@ +//! Every narrow visibility arm refuses a wider same-coordinate re-export. +//! +//! The same fixture also crosses each local and ancestor boundary, rejects the +//! private generated-module path for a public guard, and proves that an opaque +//! forwarded `vis` fragment fails closed instead of selecting a wrong arm. + +macro_rules! shallow_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +macro_rules! deep_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +pub mod shallow_ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + shallow_dollar_guard!(); + threadpak::scope_guard_version! { pub struct PublicGuard over Scope, seated in mod public_guard; } + + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use private_guard::PrivateGuard as WiderPrivate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use self_guard::SelfGuard as WiderSelf; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use in_self_guard::InSelfGuard as WiderInSelf; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use super_guard::SuperGuard as WiderSuper; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use in_super_guard::InSuperGuard as WiderInSuper; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub use relative_guard::RelativeGuard as WiderRelative; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub use crate_guard::CrateGuard as WiderCrate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub use in_crate_guard::InCrateGuard as WiderInCrate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use absolute_guard::AbsoluteGuard as WiderAbsolute; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use dollar_guard::DollarGuard as WiderDollar; + } + + fn outside_invocation_coordinate() { + let _ = owner::PrivateGuard::positioned; + let _ = owner::SelfGuard::try_cmp_same_scope; + let _ = owner::InSelfGuard::positioned; + } +} + +pub mod deep { + pub mod ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + deep_dollar_guard!(); + threadpak::scope_guard_version! { pub struct PublicGuard over Scope, seated in mod public_guard; } + + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use private_guard::PrivateGuard as WiderPrivate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use self_guard::SelfGuard as WiderSelf; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use in_self_guard::InSelfGuard as WiderInSelf; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use super_guard::SuperGuard as WiderSuper; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use in_super_guard::InSuperGuard as WiderInSuper; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use relative_guard::RelativeGuard as WiderRelative; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub use crate_guard::CrateGuard as WiderCrate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub use in_crate_guard::InCrateGuard as WiderInCrate; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use absolute_guard::AbsoluteGuard as WiderAbsolute; + #[expect(unused_imports, reason = "the deliberately illegal re-export is the reversal subject")] + pub(crate) use dollar_guard::DollarGuard as WiderDollar; + } + + fn outside_invocation_coordinate() { + let _ = owner::PrivateGuard::positioned; + let _ = owner::SelfGuard::try_cmp_same_scope; + let _ = owner::InSelfGuard::positioned; + } + } + + fn outside_parent_and_named_ancestor_reach() { + let _ = ancestor::owner::SuperGuard::positioned; + let _ = ancestor::owner::InSuperGuard::try_cmp_same_scope; + let _ = ancestor::owner::AbsoluteGuard::positioned; + let _ = ancestor::owner::DollarGuard::try_cmp_same_scope; + } +} + +macro_rules! forward_opaque_visibility { + ($visibility:vis) => { + mod forwarded { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { + $visibility struct Guard over Scope, seated in mod guard; + } + } + }; +} + +forward_opaque_visibility!(pub(super)); + +fn main() { + let _ = shallow_ancestor::owner::SuperGuard::positioned; + let _ = shallow_ancestor::owner::InSuperGuard::try_cmp_same_scope; + let _ = shallow_ancestor::owner::AbsoluteGuard::positioned; + let _ = shallow_ancestor::owner::DollarGuard::try_cmp_same_scope; + let _ = shallow_ancestor::owner::public_guard::PublicGuard::positioned; + let _ = deep::ancestor::owner::RelativeGuard::try_cmp_same_scope; + let _ = deep::ancestor::owner::public_guard::PublicGuard::try_cmp_same_scope; +} diff --git a/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.stderr b/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.stderr new file mode 100644 index 0000000..e35d2cf --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.stderr @@ -0,0 +1,736 @@ +error: scope_guard_version! requires visibility tokens at its public front door; an opaque forwarded `vis` fragment cannot be transported one module deeper + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:138:13 + | +138 | / threadpak::scope_guard_version! { +139 | | $visibility struct Guard over Scope, seated in mod guard; +140 | | } + | |_____________^ +... +145 | forward_opaque_visibility!(pub(super)); + | -------------------------------------- in this macro invocation + | + = note: this error originates in the macro `threadpak::scope_guard_version` which comes from the expansion of the macro `forward_opaque_visibility` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0365]: `PrivateGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:46:24 + | +46 | pub(crate) use private_guard::PrivateGuard as WiderPrivate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `PrivateGuard` + | + = note: consider declaring type or module `PrivateGuard` with `pub` + +error[E0365]: `SelfGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:48:24 + | +48 | pub(crate) use self_guard::SelfGuard as WiderSelf; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `SelfGuard` + | + = note: consider declaring type or module `SelfGuard` with `pub` + +error[E0365]: `InSelfGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:50:24 + | +50 | pub(crate) use in_self_guard::InSelfGuard as WiderInSelf; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `InSelfGuard` + | + = note: consider declaring type or module `InSelfGuard` with `pub` + +error[E0365]: `SuperGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:52:24 + | +52 | pub(crate) use super_guard::SuperGuard as WiderSuper; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `SuperGuard` + | + = note: consider declaring type or module `SuperGuard` with `pub` + +error[E0365]: `InSuperGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:54:24 + | +54 | pub(crate) use in_super_guard::InSuperGuard as WiderInSuper; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `InSuperGuard` + | + = note: consider declaring type or module `InSuperGuard` with `pub` + +error[E0365]: `RelativeGuard` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:56:17 + | +56 | pub use relative_guard::RelativeGuard as WiderRelative; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of crate public `RelativeGuard` + | + = note: consider declaring type or module `RelativeGuard` with `pub` + +error[E0365]: `CrateGuard` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:58:17 + | +58 | pub use crate_guard::CrateGuard as WiderCrate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of crate public `CrateGuard` + | + = note: consider declaring type or module `CrateGuard` with `pub` + +error[E0365]: `InCrateGuard` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:60:17 + | +60 | pub use in_crate_guard::InCrateGuard as WiderInCrate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of crate public `InCrateGuard` + | + = note: consider declaring type or module `InCrateGuard` with `pub` + +error[E0365]: `AbsoluteGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:62:24 + | +62 | pub(crate) use absolute_guard::AbsoluteGuard as WiderAbsolute; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `AbsoluteGuard` + | + = note: consider declaring type or module `AbsoluteGuard` with `pub` + +error[E0365]: `DollarGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:64:24 + | +64 | pub(crate) use dollar_guard::DollarGuard as WiderDollar; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `DollarGuard` + | + = note: consider declaring type or module `DollarGuard` with `pub` + +error[E0365]: `PrivateGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:96:28 + | +96 | pub(crate) use private_guard::PrivateGuard as WiderPrivate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `PrivateGuard` + | + = note: consider declaring type or module `PrivateGuard` with `pub` + +error[E0365]: `SelfGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:98:28 + | +98 | pub(crate) use self_guard::SelfGuard as WiderSelf; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `SelfGuard` + | + = note: consider declaring type or module `SelfGuard` with `pub` + +error[E0365]: `InSelfGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:100:28 + | +100 | pub(crate) use in_self_guard::InSelfGuard as WiderInSelf; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `InSelfGuard` + | + = note: consider declaring type or module `InSelfGuard` with `pub` + +error[E0365]: `SuperGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:102:28 + | +102 | pub(crate) use super_guard::SuperGuard as WiderSuper; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `SuperGuard` + | + = note: consider declaring type or module `SuperGuard` with `pub` + +error[E0365]: `InSuperGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:104:28 + | +104 | pub(crate) use in_super_guard::InSuperGuard as WiderInSuper; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `InSuperGuard` + | + = note: consider declaring type or module `InSuperGuard` with `pub` + +error[E0365]: `RelativeGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:106:28 + | +106 | pub(crate) use relative_guard::RelativeGuard as WiderRelative; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `RelativeGuard` + | + = note: consider declaring type or module `RelativeGuard` with `pub` + +error[E0365]: `CrateGuard` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:108:21 + | +108 | pub use crate_guard::CrateGuard as WiderCrate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of crate public `CrateGuard` + | + = note: consider declaring type or module `CrateGuard` with `pub` + +error[E0365]: `InCrateGuard` is only public within the crate, and cannot be re-exported outside + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:110:21 + | +110 | pub use in_crate_guard::InCrateGuard as WiderInCrate; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of crate public `InCrateGuard` + | + = note: consider declaring type or module `InCrateGuard` with `pub` + +error[E0365]: `AbsoluteGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:112:28 + | +112 | pub(crate) use absolute_guard::AbsoluteGuard as WiderAbsolute; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `AbsoluteGuard` + | + = note: consider declaring type or module `AbsoluteGuard` with `pub` + +error[E0365]: `DollarGuard` is private, and cannot be re-exported + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:114:28 + | +114 | pub(crate) use dollar_guard::DollarGuard as WiderDollar; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ re-export of private `DollarGuard` + | + = note: consider declaring type or module `DollarGuard` with `pub` + +error[E0603]: struct import `PrivateGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:68:24 + | +68 | let _ = owner::PrivateGuard::positioned; + | ^^^^^^^^^^^^ private struct import + | +note: the struct import `PrivateGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:30:9 + | +30 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `PrivateGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:30:9 + | +30 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `PrivateGuard` directly + | +68 - let _ = owner::PrivateGuard::positioned; +68 + let _ = private_guard::PrivateGuard; + | + +error[E0603]: struct import `SelfGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:69:24 + | +69 | let _ = owner::SelfGuard::try_cmp_same_scope; + | ^^^^^^^^^ private struct import + | +note: the struct import `SelfGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:31:9 + | +31 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `SelfGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:31:9 + | +31 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `SelfGuard` directly + | +69 - let _ = owner::SelfGuard::try_cmp_same_scope; +69 + let _ = self_guard::SelfGuard; + | + +error[E0603]: struct import `InSelfGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:70:24 + | +70 | let _ = owner::InSelfGuard::positioned; + | ^^^^^^^^^^^ private struct import + | +note: the struct import `InSelfGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:32:9 + | +32 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `InSelfGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:32:9 + | +32 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `InSelfGuard` directly + | +70 - let _ = owner::InSelfGuard::positioned; +70 + let _ = in_self_guard::InSelfGuard; + | + +error[E0603]: struct import `PrivateGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:118:28 + | +118 | let _ = owner::PrivateGuard::positioned; + | ^^^^^^^^^^^^ private struct import + | +note: the struct import `PrivateGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:80:13 + | + 80 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `PrivateGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:80:13 + | + 80 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `PrivateGuard` directly + | +118 - let _ = owner::PrivateGuard::positioned; +118 + let _ = private_guard::PrivateGuard; + | + +error[E0603]: struct import `SelfGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:119:28 + | +119 | let _ = owner::SelfGuard::try_cmp_same_scope; + | ^^^^^^^^^ private struct import + | +note: the struct import `SelfGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:81:13 + | + 81 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `SelfGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:81:13 + | + 81 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `SelfGuard` directly + | +119 - let _ = owner::SelfGuard::try_cmp_same_scope; +119 + let _ = self_guard::SelfGuard; + | + +error[E0603]: struct import `InSelfGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:120:28 + | +120 | let _ = owner::InSelfGuard::positioned; + | ^^^^^^^^^^^ private struct import + | +note: the struct import `InSelfGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:82:13 + | + 82 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `InSelfGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:82:13 + | + 82 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `InSelfGuard` directly + | +120 - let _ = owner::InSelfGuard::positioned; +120 + let _ = in_self_guard::InSelfGuard; + | + +error[E0603]: struct import `SuperGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:125:34 + | +125 | let _ = ancestor::owner::SuperGuard::positioned; + | ^^^^^^^^^^ private struct import + | +note: the struct import `SuperGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:83:13 + | + 83 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `SuperGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:83:13 + | + 83 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `SuperGuard` directly + | +125 - let _ = ancestor::owner::SuperGuard::positioned; +125 + let _ = super_guard::SuperGuard; + | + +error[E0603]: struct import `InSuperGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:126:34 + | +126 | let _ = ancestor::owner::InSuperGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^ private struct import + | +note: the struct import `InSuperGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:84:13 + | + 84 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `InSuperGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:84:13 + | + 84 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `InSuperGuard` directly + | +126 - let _ = ancestor::owner::InSuperGuard::try_cmp_same_scope; +126 + let _ = in_super_guard::InSuperGuard; + | + +error[E0603]: struct import `AbsoluteGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:127:34 + | +127 | let _ = ancestor::owner::AbsoluteGuard::positioned; + | ^^^^^^^^^^^^^ private struct import + | +note: the struct import `AbsoluteGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:88:13 + | + 88 | / threadpak::scope_guard_version! { + 89 | | pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + 90 | | seated in mod absolute_guard; + 91 | | } + | |_____________^ +note: ...and refers to the struct `AbsoluteGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:88:13 + | + 88 | / threadpak::scope_guard_version! { + 89 | | pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + 90 | | seated in mod absolute_guard; + 91 | | } + | |_____________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `AbsoluteGuard` directly + | +127 - let _ = ancestor::owner::AbsoluteGuard::positioned; +127 + let _ = absolute_guard::AbsoluteGuard; + | + +error[E0603]: struct import `DollarGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:128:34 + | +128 | let _ = ancestor::owner::DollarGuard::try_cmp_same_scope; + | ^^^^^^^^^^^ private struct import + | +note: the struct import `DollarGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:18:9 + | + 18 | / threadpak::scope_guard_version! { + 19 | | pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + 20 | | seated in mod dollar_guard; + 21 | | } + | |_________^ +... + 92 | deep_dollar_guard!(); + | -------------------- in this macro invocation +note: ...and refers to the struct `DollarGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:18:9 + | + 18 | / threadpak::scope_guard_version! { + 19 | | pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + 20 | | seated in mod dollar_guard; + 21 | | } + | |_________^ +... + 92 | deep_dollar_guard!(); + | -------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `deep_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `DollarGuard` directly + | +128 - let _ = ancestor::owner::DollarGuard::try_cmp_same_scope; +128 + let _ = dollar_guard::DollarGuard; + | + +error[E0603]: struct import `SuperGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:148:38 + | +148 | let _ = shallow_ancestor::owner::SuperGuard::positioned; + | ^^^^^^^^^^ private struct import + | +note: the struct import `SuperGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:33:9 + | + 33 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `SuperGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:33:9 + | + 33 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `SuperGuard` directly + | +148 - let _ = shallow_ancestor::owner::SuperGuard::positioned; +148 + let _ = super_guard::SuperGuard; + | + +error[E0603]: struct import `InSuperGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:149:38 + | +149 | let _ = shallow_ancestor::owner::InSuperGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^ private struct import + | +note: the struct import `InSuperGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:34:9 + | + 34 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `InSuperGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:34:9 + | + 34 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `InSuperGuard` directly + | +149 - let _ = shallow_ancestor::owner::InSuperGuard::try_cmp_same_scope; +149 + let _ = in_super_guard::InSuperGuard; + | + +error[E0603]: struct import `AbsoluteGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:150:38 + | +150 | let _ = shallow_ancestor::owner::AbsoluteGuard::positioned; + | ^^^^^^^^^^^^^ private struct import + | +note: the struct import `AbsoluteGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:38:9 + | + 38 | / threadpak::scope_guard_version! { + 39 | | pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + 40 | | seated in mod absolute_guard; + 41 | | } + | |_________^ +note: ...and refers to the struct `AbsoluteGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:38:9 + | + 38 | / threadpak::scope_guard_version! { + 39 | | pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + 40 | | seated in mod absolute_guard; + 41 | | } + | |_________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `AbsoluteGuard` directly + | +150 - let _ = shallow_ancestor::owner::AbsoluteGuard::positioned; +150 + let _ = absolute_guard::AbsoluteGuard; + | + +error[E0603]: struct import `DollarGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:151:38 + | +151 | let _ = shallow_ancestor::owner::DollarGuard::try_cmp_same_scope; + | ^^^^^^^^^^^ private struct import + | +note: the struct import `DollarGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:9:9 + | + 9 | / threadpak::scope_guard_version! { + 10 | | pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + 11 | | seated in mod dollar_guard; + 12 | | } + | |_________^ +... + 42 | shallow_dollar_guard!(); + | ----------------------- in this macro invocation +note: ...and refers to the struct `DollarGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:9:9 + | + 9 | / threadpak::scope_guard_version! { + 10 | | pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + 11 | | seated in mod dollar_guard; + 12 | | } + | |_________^ +... + 42 | shallow_dollar_guard!(); + | ----------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `shallow_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `DollarGuard` directly + | +151 - let _ = shallow_ancestor::owner::DollarGuard::try_cmp_same_scope; +151 + let _ = dollar_guard::DollarGuard; + | + +error[E0603]: module `public_guard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:152:38 + | +152 | let _ = shallow_ancestor::owner::public_guard::PublicGuard::positioned; + | ^^^^^^^^^^^^ private module + | +note: the module `public_guard` is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:43:9 + | + 43 | threadpak::scope_guard_version! { pub struct PublicGuard over Scope, seated in mod public_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider importing this struct through its public re-export instead + | +152 - let _ = shallow_ancestor::owner::public_guard::PublicGuard::positioned; +152 + let _ = shallow_ancestor::owner::PublicGuard::positioned; + | + +error[E0603]: struct import `RelativeGuard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:153:36 + | +153 | let _ = deep::ancestor::owner::RelativeGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^ private struct import + | +note: the struct import `RelativeGuard` is defined here... + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:85:13 + | + 85 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `RelativeGuard` which is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:85:13 + | + 85 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: import `RelativeGuard` directly + | +153 - let _ = deep::ancestor::owner::RelativeGuard::try_cmp_same_scope; +153 + let _ = relative_guard::RelativeGuard; + | + +error[E0603]: module `public_guard` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:154:36 + | +154 | let _ = deep::ancestor::owner::public_guard::PublicGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^ private module + | +note: the module `public_guard` is defined here + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:93:13 + | + 93 | threadpak::scope_guard_version! { pub struct PublicGuard over Scope, seated in mod public_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider importing this struct through its public re-export instead + | +154 - let _ = deep::ancestor::owner::public_guard::PublicGuard::try_cmp_same_scope; +154 + let _ = deep::ancestor::owner::PublicGuard::try_cmp_same_scope; + | + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:68:38 + | +30 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ------------------------------------------------------------------------------------------------ private associated function defined here +... +68 | let _ = owner::PrivateGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:69:35 + | +31 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ---------------------------------------------------------------------------------------------------- private method defined here +... +69 | let _ = owner::SelfGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:70:37 + | +32 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ------------------------------------------------------------------------------------------------------------ private associated function defined here +... +70 | let _ = owner::InSelfGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:118:42 + | + 80 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ------------------------------------------------------------------------------------------------ private associated function defined here +... +118 | let _ = owner::PrivateGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:119:39 + | + 81 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ---------------------------------------------------------------------------------------------------- private method defined here +... +119 | let _ = owner::SelfGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:120:41 + | + 82 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ------------------------------------------------------------------------------------------------------------ private associated function defined here +... +120 | let _ = owner::InSelfGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:125:46 + | + 83 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ------------------------------------------------------------------------------------------------------- private associated function defined here +... +125 | let _ = ancestor::owner::SuperGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:126:48 + | + 84 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | --------------------------------------------------------------------------------------------------------------- private method defined here +... +126 | let _ = ancestor::owner::InSuperGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:127:49 + | + 88 | / threadpak::scope_guard_version! { + 89 | | pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + 90 | | seated in mod absolute_guard; + 91 | | } + | |_____________- private associated function defined here +... +127 | let _ = ancestor::owner::AbsoluteGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:128:47 + | + 18 | / threadpak::scope_guard_version! { + 19 | | pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + 20 | | seated in mod dollar_guard; + 21 | | } + | |_________- private method defined here +... +128 | let _ = ancestor::owner::DollarGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:148:50 + | + 33 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ------------------------------------------------------------------------------------------------------- private associated function defined here +... +148 | let _ = shallow_ancestor::owner::SuperGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:149:52 + | + 34 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | --------------------------------------------------------------------------------------------------------------- private method defined here +... +149 | let _ = shallow_ancestor::owner::InSuperGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: associated function `positioned` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:150:53 + | + 38 | / threadpak::scope_guard_version! { + 39 | | pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + 40 | | seated in mod absolute_guard; + 41 | | } + | |_________- private associated function defined here +... +150 | let _ = shallow_ancestor::owner::AbsoluteGuard::positioned; + | ^^^^^^^^^^ private associated function + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:151:51 + | + 9 | / threadpak::scope_guard_version! { + 10 | | pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + 11 | | seated in mod dollar_guard; + 12 | | } + | |_________- private method defined here +... +151 | let _ = shallow_ancestor::owner::DollarGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method + +error[E0624]: method `try_cmp_same_scope` is private + --> tests/compile-fail/a-scope-guard-reexport-cannot-widen-reach.rs:153:51 + | + 85 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ----------------------------------------------------------------------------------------------------------------------- private method defined here +... +153 | let _ = deep::ancestor::owner::RelativeGuard::try_cmp_same_scope; + | ^^^^^^^^^^^^^^^^^^ private method diff --git a/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs b/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs new file mode 100644 index 0000000..faff996 --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs @@ -0,0 +1,89 @@ +//! Every narrow visibility arm refuses a wider same-coordinate signature. + +#![deny(private_interfaces)] + +macro_rules! shallow_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +macro_rules! deep_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +pub mod shallow_ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + shallow_dollar_guard!(); + + pub(crate) fn wider_private(_: private_guard::PrivateGuard) {} + pub(crate) fn wider_self(_: self_guard::SelfGuard) {} + pub(crate) fn wider_in_self(_: in_self_guard::InSelfGuard) {} + pub(crate) fn wider_super(_: super_guard::SuperGuard) {} + pub(crate) fn wider_in_super(_: in_super_guard::InSuperGuard) {} + pub fn wider_relative(_: relative_guard::RelativeGuard) {} + pub fn wider_crate(_: crate_guard::CrateGuard) {} + pub fn wider_in_crate(_: in_crate_guard::InCrateGuard) {} + pub(crate) fn wider_absolute(_: absolute_guard::AbsoluteGuard) {} + pub(crate) fn wider_dollar(_: dollar_guard::DollarGuard) {} + } +} + +pub mod deep { + pub mod ancestor { + pub mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + deep_dollar_guard!(); + + pub(crate) fn wider_private(_: private_guard::PrivateGuard) {} + pub(crate) fn wider_self(_: self_guard::SelfGuard) {} + pub(crate) fn wider_in_self(_: in_self_guard::InSelfGuard) {} + pub(crate) fn wider_super(_: super_guard::SuperGuard) {} + pub(crate) fn wider_in_super(_: in_super_guard::InSuperGuard) {} + pub(crate) fn wider_relative(_: relative_guard::RelativeGuard) {} + pub fn wider_crate(_: crate_guard::CrateGuard) {} + pub fn wider_in_crate(_: in_crate_guard::InCrateGuard) {} + pub(crate) fn wider_absolute(_: absolute_guard::AbsoluteGuard) {} + pub(crate) fn wider_dollar(_: dollar_guard::DollarGuard) {} + } + } +} + +fn main() {} diff --git a/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.stderr b/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.stderr new file mode 100644 index 0000000..fad4b0d --- /dev/null +++ b/testpak/tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.stderr @@ -0,0 +1,282 @@ +error: type `shallow_ancestor::owner::private_guard::PrivateGuard` is more private than the item `shallow_ancestor::owner::wider_private` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:42:9 + | +42 | pub(crate) fn wider_private(_: private_guard::PrivateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_private` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::private_guard::PrivateGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:28:9 + | +28 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: the lint level is defined here + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:3:9 + | + 3 | #![deny(private_interfaces)] + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::self_guard::SelfGuard` is more private than the item `shallow_ancestor::owner::wider_self` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:43:9 + | +43 | pub(crate) fn wider_self(_: self_guard::SelfGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_self` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::self_guard::SelfGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:29:9 + | +29 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_self_guard::InSelfGuard` is more private than the item `shallow_ancestor::owner::wider_in_self` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:44:9 + | +44 | pub(crate) fn wider_in_self(_: in_self_guard::InSelfGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_in_self` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::in_self_guard::InSelfGuard` is only usable at visibility `pub(in crate::shallow_ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:30:9 + | +30 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::super_guard::SuperGuard` is more private than the item `shallow_ancestor::owner::wider_super` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:45:9 + | +45 | pub(crate) fn wider_super(_: super_guard::SuperGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_super` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::super_guard::SuperGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:31:9 + | +31 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_super_guard::InSuperGuard` is more private than the item `shallow_ancestor::owner::wider_in_super` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:46:9 + | +46 | pub(crate) fn wider_in_super(_: in_super_guard::InSuperGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_in_super` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::in_super_guard::InSuperGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:32:9 + | +32 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::relative_guard::RelativeGuard` is more private than the item `shallow_ancestor::owner::wider_relative` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:47:9 + | +47 | pub fn wider_relative(_: relative_guard::RelativeGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_relative` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::relative_guard::RelativeGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:33:9 + | +33 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::crate_guard::CrateGuard` is more private than the item `shallow_ancestor::owner::wider_crate` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:48:9 + | +48 | pub fn wider_crate(_: crate_guard::CrateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_crate` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::crate_guard::CrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:34:9 + | +34 | threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::in_crate_guard::InCrateGuard` is more private than the item `shallow_ancestor::owner::wider_in_crate` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:49:9 + | +49 | pub fn wider_in_crate(_: in_crate_guard::InCrateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_in_crate` is reachable at visibility `pub` + | +note: but type `shallow_ancestor::owner::in_crate_guard::InCrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:35:9 + | +35 | threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::absolute_guard::AbsoluteGuard` is more private than the item `shallow_ancestor::owner::wider_absolute` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:50:9 + | +50 | pub(crate) fn wider_absolute(_: absolute_guard::AbsoluteGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_absolute` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::absolute_guard::AbsoluteGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:36:9 + | +36 | / threadpak::scope_guard_version! { +37 | | pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, +38 | | seated in mod absolute_guard; +39 | | } + | |_________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `shallow_ancestor::owner::dollar_guard::DollarGuard` is more private than the item `shallow_ancestor::owner::wider_dollar` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:51:9 + | +51 | pub(crate) fn wider_dollar(_: dollar_guard::DollarGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `shallow_ancestor::owner::wider_dollar` is reachable at visibility `pub(crate)` + | +note: but type `shallow_ancestor::owner::dollar_guard::DollarGuard` is only usable at visibility `pub(in crate::shallow_ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:7:9 + | + 7 | / threadpak::scope_guard_version! { + 8 | | pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + 9 | | seated in mod dollar_guard; +10 | | } + | |_________^ +... +40 | shallow_dollar_guard!(); + | ----------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `shallow_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::private_guard::PrivateGuard` is more private than the item `ancestor::owner::wider_private` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:75:13 + | +75 | pub(crate) fn wider_private(_: private_guard::PrivateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_private` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::private_guard::PrivateGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:61:13 + | +61 | threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::self_guard::SelfGuard` is more private than the item `ancestor::owner::wider_self` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:76:13 + | +76 | pub(crate) fn wider_self(_: self_guard::SelfGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_self` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::self_guard::SelfGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:62:13 + | +62 | threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_self_guard::InSelfGuard` is more private than the item `ancestor::owner::wider_in_self` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:77:13 + | +77 | pub(crate) fn wider_in_self(_: in_self_guard::InSelfGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_in_self` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::in_self_guard::InSelfGuard` is only usable at visibility `pub(in crate::deep::ancestor::owner)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:63:13 + | +63 | threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::super_guard::SuperGuard` is more private than the item `ancestor::owner::wider_super` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:78:13 + | +78 | pub(crate) fn wider_super(_: super_guard::SuperGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_super` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::super_guard::SuperGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:64:13 + | +64 | threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_super_guard::InSuperGuard` is more private than the item `ancestor::owner::wider_in_super` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:79:13 + | +79 | pub(crate) fn wider_in_super(_: in_super_guard::InSuperGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_in_super` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::in_super_guard::InSuperGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:65:13 + | +65 | threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::relative_guard::RelativeGuard` is more private than the item `ancestor::owner::wider_relative` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:80:13 + | +80 | pub(crate) fn wider_relative(_: relative_guard::RelativeGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_relative` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::relative_guard::RelativeGuard` is only usable at visibility `pub(in crate::deep)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:66:13 + | +66 | threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::crate_guard::CrateGuard` is more private than the item `ancestor::owner::wider_crate` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:81:13 + | +81 | pub fn wider_crate(_: crate_guard::CrateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_crate` is reachable at visibility `pub` + | +note: but type `ancestor::owner::crate_guard::CrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:67:13 + | +67 | threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::in_crate_guard::InCrateGuard` is more private than the item `ancestor::owner::wider_in_crate` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:82:13 + | +82 | pub fn wider_in_crate(_: in_crate_guard::InCrateGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_in_crate` is reachable at visibility `pub` + | +note: but type `ancestor::owner::in_crate_guard::InCrateGuard` is only usable at visibility `pub(crate)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:68:13 + | +68 | threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::absolute_guard::AbsoluteGuard` is more private than the item `ancestor::owner::wider_absolute` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:83:13 + | +83 | pub(crate) fn wider_absolute(_: absolute_guard::AbsoluteGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_absolute` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::absolute_guard::AbsoluteGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:69:13 + | +69 | / threadpak::scope_guard_version! { +70 | | pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, +71 | | seated in mod absolute_guard; +72 | | } + | |_____________^ + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: type `ancestor::owner::dollar_guard::DollarGuard` is more private than the item `ancestor::owner::wider_dollar` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:84:13 + | +84 | pub(crate) fn wider_dollar(_: dollar_guard::DollarGuard) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function `ancestor::owner::wider_dollar` is reachable at visibility `pub(crate)` + | +note: but type `ancestor::owner::dollar_guard::DollarGuard` is only usable at visibility `pub(in crate::deep::ancestor)` + --> tests/compile-fail/a-scope-guard-signature-cannot-widen-reach.rs:16:9 + | +16 | / threadpak::scope_guard_version! { +17 | | pub(in $crate::deep::ancestor) struct DollarGuard over Scope, +18 | | seated in mod dollar_guard; +19 | | } + | |_________^ +... +73 | deep_dollar_guard!(); + | -------------------- in this macro invocation + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `deep_dollar_guard` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/testpak/tests/compile-fail/a-stamped-representation-cannot-be-laundered.stderr b/testpak/tests/compile-fail/a-stamped-representation-cannot-be-laundered.stderr index fe4c9aa..dca0438 100644 --- a/testpak/tests/compile-fail/a-stamped-representation-cannot-be-laundered.stderr +++ b/testpak/tests/compile-fail/a-stamped-representation-cannot-be-laundered.stderr @@ -13,12 +13,12 @@ note: constructor is not visible here due to private fields 30 | | pub struct RoleBVersion over OneScopeId, seated in mod role_b_version; 31 | | } | |_____^ private field - = note: this error originates in the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider making the field publicly accessible --> $WORKSPACE/src/02_identity/mod.rs | - | $vis struct $name(pub $crate::identity::AuthorityPosition<$scope>); - | +++ + | $internal_vis struct $name(pub $crate::identity::AuthorityPosition<$scope>); + | +++ error[E0616]: field `0` of struct `RoleAVersion` is private --> tests/compile-fail/a-stamped-representation-cannot-be-laundered.rs:36:45 diff --git a/testpak/tests/compile-fail/cross-frame-comparison-on-a-production-guard.stderr b/testpak/tests/compile-fail/cross-frame-comparison-on-a-production-guard.stderr index 9a4f6c9..faf2329 100644 --- a/testpak/tests/compile-fail/cross-frame-comparison-on-a-production-guard.stderr +++ b/testpak/tests/compile-fail/cross-frame-comparison-on-a-production-guard.stderr @@ -9,8 +9,8 @@ error[E0369]: binary operation `<` cannot be applied to type `&FrameVersion` note: `FrameVersion` does not implement `PartialOrd` --> $WORKSPACE/src/02_identity/mod.rs | - | $vis struct $name($crate::identity::AuthorityPosition<$scope>); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `FrameVersion` is defined in another crate + | $internal_vis struct $name($crate::identity::AuthorityPosition<$scope>); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `FrameVersion` is defined in another crate | ::: $WORKSPACE/src/11_navigation/types.rs | @@ -22,7 +22,7 @@ note: `FrameVersion` does not implement `PartialOrd` | | pub struct FrameVersion over ReferenceFrameId, seated in mod frame_version; | | } | |_- in this macro invocation - = note: this error originates in the macro `crate::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `crate::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: the method `cmp` exists for reference `&FrameVersion`, but its trait bounds were not satisfied --> tests/compile-fail/cross-frame-comparison-on-a-production-guard.rs:43:28 @@ -32,8 +32,8 @@ error[E0599]: the method `cmp` exists for reference `&FrameVersion`, but its tra | ::: $WORKSPACE/src/02_identity/mod.rs | - | $vis struct $name($crate::identity::AuthorityPosition<$scope>); - | --------------------------------------------------------------- doesn't satisfy `FrameVersion: Iterator` or `FrameVersion: Ord` + | $internal_vis struct $name($crate::identity::AuthorityPosition<$scope>); + | ------------------------------------------------------------------------ doesn't satisfy `FrameVersion: Iterator` or `FrameVersion: Ord` | = note: the following trait bounds were not satisfied: `FrameVersion: Ord` diff --git a/testpak/tests/compile-fail/cross-scope-comparison-on-a-stamped-guard.stderr b/testpak/tests/compile-fail/cross-scope-comparison-on-a-stamped-guard.stderr index b776397..530de66 100644 --- a/testpak/tests/compile-fail/cross-scope-comparison-on-a-stamped-guard.stderr +++ b/testpak/tests/compile-fail/cross-scope-comparison-on-a-stamped-guard.stderr @@ -16,4 +16,4 @@ note: method defined here 17 | | pub struct AlphaVersion over AlphaScopeId, seated in mod alpha_version; 18 | | } | |_^ - = note: this error originates in the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::scope_guard_version` which comes from the expansion of the macro `threadpak::scope_guard_version` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/testpak/tests/scope_guard_alias_visibility.rs b/testpak/tests/scope_guard_alias_visibility.rs new file mode 100644 index 0000000..4aa5057 --- /dev/null +++ b/testpak/tests/scope_guard_alias_visibility.rs @@ -0,0 +1,148 @@ +//! Positive controls for same-reach scope-guard type aliases. + +macro_rules! shallow_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +macro_rules! deep_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +mod shallow_ancestor { + pub(crate) mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + shallow_dollar_guard!(); + + type PrivateAlias = private_guard::PrivateGuard; + type SelfAlias = self_guard::SelfGuard; + type InSelfAlias = in_self_guard::InSelfGuard; + pub(super) type SuperAlias = super_guard::SuperGuard; + pub(super) type InSuperAlias = in_super_guard::InSuperGuard; + pub(in super::super) type RelativeAlias = relative_guard::RelativeGuard; + pub(crate) type CrateAlias = crate_guard::CrateGuard; + pub(crate) type InCrateAlias = in_crate_guard::InCrateGuard; + pub(in crate::shallow_ancestor) type AbsoluteAlias = absolute_guard::AbsoluteGuard; + pub(in crate::shallow_ancestor) type DollarAlias = dollar_guard::DollarGuard; + + pub(super) fn local_aliases() { + let _ = PrivateAlias::positioned; + let _ = SelfAlias::positioned; + let _ = InSelfAlias::positioned; + let _ = SuperAlias::positioned; + let _ = InSuperAlias::positioned; + let _ = RelativeAlias::positioned; + let _ = CrateAlias::positioned; + let _ = InCrateAlias::positioned; + let _ = AbsoluteAlias::positioned; + let _ = DollarAlias::positioned; + } + } + + pub(super) fn ancestor_aliases() { + owner::local_aliases(); + let _ = owner::SuperAlias::try_cmp_same_scope; + let _ = owner::InSuperAlias::try_cmp_same_scope; + let _ = owner::RelativeAlias::try_cmp_same_scope; + let _ = owner::CrateAlias::try_cmp_same_scope; + let _ = owner::InCrateAlias::try_cmp_same_scope; + let _ = owner::AbsoluteAlias::try_cmp_same_scope; + let _ = owner::DollarAlias::try_cmp_same_scope; + } +} + +mod deep { + pub(crate) mod ancestor { + pub(crate) mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + deep_dollar_guard!(); + + type PrivateAlias = private_guard::PrivateGuard; + type SelfAlias = self_guard::SelfGuard; + type InSelfAlias = in_self_guard::InSelfGuard; + pub(super) type SuperAlias = super_guard::SuperGuard; + pub(super) type InSuperAlias = in_super_guard::InSuperGuard; + pub(in super::super) type RelativeAlias = relative_guard::RelativeGuard; + pub(crate) type CrateAlias = crate_guard::CrateGuard; + pub(crate) type InCrateAlias = in_crate_guard::InCrateGuard; + pub(in crate::deep::ancestor) type AbsoluteAlias = absolute_guard::AbsoluteGuard; + pub(in crate::deep::ancestor) type DollarAlias = dollar_guard::DollarGuard; + + pub(super) fn local_aliases() { + let _ = PrivateAlias::positioned; + let _ = SelfAlias::positioned; + let _ = InSelfAlias::positioned; + let _ = SuperAlias::positioned; + let _ = InSuperAlias::positioned; + let _ = RelativeAlias::positioned; + let _ = CrateAlias::positioned; + let _ = InCrateAlias::positioned; + let _ = AbsoluteAlias::positioned; + let _ = DollarAlias::positioned; + } + } + + pub(crate) fn ancestor_aliases() { + owner::local_aliases(); + let _ = owner::SuperAlias::try_cmp_same_scope; + let _ = owner::InSuperAlias::try_cmp_same_scope; + let _ = owner::RelativeAlias::try_cmp_same_scope; + let _ = owner::CrateAlias::try_cmp_same_scope; + let _ = owner::InCrateAlias::try_cmp_same_scope; + let _ = owner::AbsoluteAlias::try_cmp_same_scope; + let _ = owner::DollarAlias::try_cmp_same_scope; + } + } +} + +/// Every narrow front spelling permits a type alias at exactly its own reach, +/// at both invocation depths. +/// +/// green: identity.scope-guard-alias-cannot-widen +#[test] +fn same_reach_scope_guard_aliases_are_lawful() { + shallow_ancestor::ancestor_aliases(); + deep::ancestor::ancestor_aliases(); + let _ = shallow_ancestor::owner::RelativeAlias::try_cmp_same_scope; + let _ = shallow_ancestor::owner::CrateAlias::try_cmp_same_scope; + let _ = shallow_ancestor::owner::InCrateAlias::try_cmp_same_scope; + let _ = deep::ancestor::owner::CrateAlias::try_cmp_same_scope; + let _ = deep::ancestor::owner::InCrateAlias::try_cmp_same_scope; +} diff --git a/testpak/tests/scope_guard_signature_visibility.rs b/testpak/tests/scope_guard_signature_visibility.rs new file mode 100644 index 0000000..8cf68a3 --- /dev/null +++ b/testpak/tests/scope_guard_signature_visibility.rs @@ -0,0 +1,148 @@ +//! Positive controls for same-reach scope guards in function signatures. + +macro_rules! shallow_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::shallow_ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +macro_rules! deep_dollar_guard { + () => { + threadpak::scope_guard_version! { + pub(in $crate::deep::ancestor) struct DollarGuard over Scope, + seated in mod dollar_guard; + } + }; +} + +mod shallow_ancestor { + pub(crate) mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::shallow_ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + shallow_dollar_guard!(); + + fn private_signature(_: private_guard::PrivateGuard) {} + fn self_signature(_: self_guard::SelfGuard) {} + fn in_self_signature(_: in_self_guard::InSelfGuard) {} + pub(super) fn super_signature(_: super_guard::SuperGuard) {} + pub(super) fn in_super_signature(_: in_super_guard::InSuperGuard) {} + pub(in super::super) fn relative_signature(_: relative_guard::RelativeGuard) {} + pub(crate) fn crate_signature(_: crate_guard::CrateGuard) {} + pub(crate) fn in_crate_signature(_: in_crate_guard::InCrateGuard) {} + pub(in crate::shallow_ancestor) fn absolute_signature(_: absolute_guard::AbsoluteGuard) {} + pub(in crate::shallow_ancestor) fn dollar_signature(_: dollar_guard::DollarGuard) {} + + pub(super) fn local_signatures() { + let _ = private_signature; + let _ = self_signature; + let _ = in_self_signature; + let _ = super_signature; + let _ = in_super_signature; + let _ = relative_signature; + let _ = crate_signature; + let _ = in_crate_signature; + let _ = absolute_signature; + let _ = dollar_signature; + } + } + + pub(super) fn ancestor_signatures() { + owner::local_signatures(); + let _ = owner::super_signature; + let _ = owner::in_super_signature; + let _ = owner::relative_signature; + let _ = owner::crate_signature; + let _ = owner::in_crate_signature; + let _ = owner::absolute_signature; + let _ = owner::dollar_signature; + } +} + +mod deep { + pub(crate) mod ancestor { + pub(crate) mod owner { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { struct PrivateGuard over Scope, seated in mod private_guard; } + threadpak::scope_guard_version! { pub(self) struct SelfGuard over Scope, seated in mod self_guard; } + threadpak::scope_guard_version! { pub(in self) struct InSelfGuard over Scope, seated in mod in_self_guard; } + threadpak::scope_guard_version! { pub(super) struct SuperGuard over Scope, seated in mod super_guard; } + threadpak::scope_guard_version! { pub(in super) struct InSuperGuard over Scope, seated in mod in_super_guard; } + threadpak::scope_guard_version! { pub(in super::super) struct RelativeGuard over Scope, seated in mod relative_guard; } + threadpak::scope_guard_version! { pub(crate) struct CrateGuard over Scope, seated in mod crate_guard; } + threadpak::scope_guard_version! { pub(in crate) struct InCrateGuard over Scope, seated in mod in_crate_guard; } + threadpak::scope_guard_version! { + pub(in crate::deep::ancestor) struct AbsoluteGuard over Scope, + seated in mod absolute_guard; + } + deep_dollar_guard!(); + + fn private_signature(_: private_guard::PrivateGuard) {} + fn self_signature(_: self_guard::SelfGuard) {} + fn in_self_signature(_: in_self_guard::InSelfGuard) {} + pub(super) fn super_signature(_: super_guard::SuperGuard) {} + pub(super) fn in_super_signature(_: in_super_guard::InSuperGuard) {} + pub(in super::super) fn relative_signature(_: relative_guard::RelativeGuard) {} + pub(crate) fn crate_signature(_: crate_guard::CrateGuard) {} + pub(crate) fn in_crate_signature(_: in_crate_guard::InCrateGuard) {} + pub(in crate::deep::ancestor) fn absolute_signature(_: absolute_guard::AbsoluteGuard) {} + pub(in crate::deep::ancestor) fn dollar_signature(_: dollar_guard::DollarGuard) {} + + pub(super) fn local_signatures() { + let _ = private_signature; + let _ = self_signature; + let _ = in_self_signature; + let _ = super_signature; + let _ = in_super_signature; + let _ = relative_signature; + let _ = crate_signature; + let _ = in_crate_signature; + let _ = absolute_signature; + let _ = dollar_signature; + } + } + + pub(crate) fn ancestor_signatures() { + owner::local_signatures(); + let _ = owner::super_signature; + let _ = owner::in_super_signature; + let _ = owner::relative_signature; + let _ = owner::crate_signature; + let _ = owner::in_crate_signature; + let _ = owner::absolute_signature; + let _ = owner::dollar_signature; + } + } +} + +/// Every narrow front spelling permits a public-signature road at exactly its +/// own reach, at both invocation depths. +/// +/// green: identity.scope-guard-signature-cannot-widen +#[test] +fn same_reach_scope_guard_signatures_are_lawful() { + shallow_ancestor::ancestor_signatures(); + deep::ancestor::ancestor_signatures(); + let _ = shallow_ancestor::owner::relative_signature; + let _ = shallow_ancestor::owner::crate_signature; + let _ = shallow_ancestor::owner::in_crate_signature; + let _ = deep::ancestor::owner::crate_signature; + let _ = deep::ancestor::owner::in_crate_signature; +} diff --git a/testpak/tests/scope_guard_visibility.rs b/testpak/tests/scope_guard_visibility.rs new file mode 100644 index 0000000..15305fd --- /dev/null +++ b/testpak/tests/scope_guard_visibility.rs @@ -0,0 +1,357 @@ +//! Positive compiler evidence for caller-relative scope-guard visibility. +//! +//! The population is the public macro's admitted front grammar, exercised at +//! two invocation depths. Each declaration uses both emitted operations inside +//! its lawful reach; the compile-refusal twin crosses each narrower boundary. + +use threadpak::identity::{AuthorityPosition, OrderComparison}; + +fn assert_surface( + _: fn(AuthorityPosition) -> Guard, + _: fn(&Guard, &Guard) -> Result, +) { +} + +macro_rules! crate_relative_scope_guard { + ($name:ident, $scope:ty, $home:ident) => { + threadpak::scope_guard_version! { + /// An absolute visibility rooted by the invoking macro's crate. + pub(in $crate::scope_guard_visibility) struct $name over $scope, + seated in mod $home; + } + }; +} + +mod scope_guard_visibility { + use super::assert_surface; + + pub(crate) mod shallow { + use super::assert_surface; + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { + struct Private over Scope, seated in mod private; + } + threadpak::scope_guard_version! { + pub(self) struct SelfShort over Scope, seated in mod self_short; + } + threadpak::scope_guard_version! { + pub(in self) struct SelfLong over Scope, seated in mod self_long; + } + threadpak::scope_guard_version! { + pub(super) struct SuperShort over Scope, seated in mod super_short; + } + threadpak::scope_guard_version! { + pub(in super) struct SuperLong over Scope, seated in mod super_long; + } + threadpak::scope_guard_version! { + pub(in super::super) struct RelativeAncestor over Scope, + seated in mod relative_ancestor; + } + threadpak::scope_guard_version! { + pub(crate) struct CrateShort over Scope, seated in mod crate_short; + } + threadpak::scope_guard_version! { + pub(in crate) struct CrateLong over Scope, seated in mod crate_long; + } + threadpak::scope_guard_version! { + pub(in crate::scope_guard_visibility) struct AbsoluteAncestor over Scope, + seated in mod absolute_ancestor; + } + crate_relative_scope_guard!(DollarCrateAncestor, Scope, dollar_crate_ancestor); + + pub(in crate::scope_guard_visibility) use absolute_ancestor::AbsoluteAncestor as AbsoluteSameReach; + pub(crate) use crate_long::CrateLong as CrateLongSameReach; + pub(crate) use crate_short::CrateShort as CrateShortSameReach; + pub(in crate::scope_guard_visibility) use dollar_crate_ancestor::DollarCrateAncestor as DollarCrateSameReach; + use private::Private as PrivateSameReach; + pub(in super::super) use relative_ancestor::RelativeAncestor as RelativeSameReach; + use self_long::SelfLong as SelfLongSameReach; + use self_short::SelfShort as SelfShortSameReach; + pub(super) use super_long::SuperLong as SuperLongSameReach; + pub(super) use super_short::SuperShort as SuperShortSameReach; + + pub(super) fn local_surfaces() { + assert_surface(Private::positioned, Private::try_cmp_same_scope); + assert_surface( + PrivateSameReach::positioned, + PrivateSameReach::try_cmp_same_scope, + ); + assert_surface(SelfShort::positioned, SelfShort::try_cmp_same_scope); + assert_surface( + SelfShortSameReach::positioned, + SelfShortSameReach::try_cmp_same_scope, + ); + assert_surface(SelfLong::positioned, SelfLong::try_cmp_same_scope); + assert_surface( + SelfLongSameReach::positioned, + SelfLongSameReach::try_cmp_same_scope, + ); + assert_surface(SuperShort::positioned, SuperShort::try_cmp_same_scope); + assert_surface( + SuperShortSameReach::positioned, + SuperShortSameReach::try_cmp_same_scope, + ); + assert_surface(SuperLong::positioned, SuperLong::try_cmp_same_scope); + assert_surface( + SuperLongSameReach::positioned, + SuperLongSameReach::try_cmp_same_scope, + ); + assert_surface( + RelativeAncestor::positioned, + RelativeAncestor::try_cmp_same_scope, + ); + assert_surface( + RelativeSameReach::positioned, + RelativeSameReach::try_cmp_same_scope, + ); + assert_surface(CrateShort::positioned, CrateShort::try_cmp_same_scope); + assert_surface( + CrateShortSameReach::positioned, + CrateShortSameReach::try_cmp_same_scope, + ); + assert_surface(CrateLong::positioned, CrateLong::try_cmp_same_scope); + assert_surface( + CrateLongSameReach::positioned, + CrateLongSameReach::try_cmp_same_scope, + ); + assert_surface( + AbsoluteAncestor::positioned, + AbsoluteAncestor::try_cmp_same_scope, + ); + assert_surface( + AbsoluteSameReach::positioned, + AbsoluteSameReach::try_cmp_same_scope, + ); + assert_surface( + DollarCrateAncestor::positioned, + DollarCrateAncestor::try_cmp_same_scope, + ); + assert_surface( + DollarCrateSameReach::positioned, + DollarCrateSameReach::try_cmp_same_scope, + ); + } + } + + pub(crate) mod deep { + use super::assert_surface; + + pub(crate) mod inner { + use super::assert_surface; + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub(crate) struct Scope; + + threadpak::scope_guard_version! { + struct Private over Scope, seated in mod private; + } + threadpak::scope_guard_version! { + pub(self) struct SelfShort over Scope, seated in mod self_short; + } + threadpak::scope_guard_version! { + pub(in self) struct SelfLong over Scope, seated in mod self_long; + } + threadpak::scope_guard_version! { + pub(super) struct SuperShort over Scope, seated in mod super_short; + } + threadpak::scope_guard_version! { + pub(in super) struct SuperLong over Scope, seated in mod super_long; + } + threadpak::scope_guard_version! { + pub(in super::super) struct RelativeAncestor over Scope, + seated in mod relative_ancestor; + } + threadpak::scope_guard_version! { + pub(crate) struct CrateShort over Scope, seated in mod crate_short; + } + threadpak::scope_guard_version! { + pub(in crate) struct CrateLong over Scope, seated in mod crate_long; + } + threadpak::scope_guard_version! { + pub(in crate::scope_guard_visibility::deep) struct AbsoluteAncestor over Scope, + seated in mod absolute_ancestor; + } + crate_relative_scope_guard!(DollarCrateAncestor, Scope, dollar_crate_ancestor); + + pub(in crate::scope_guard_visibility::deep) use absolute_ancestor::AbsoluteAncestor as AbsoluteSameReach; + pub(crate) use crate_long::CrateLong as CrateLongSameReach; + pub(crate) use crate_short::CrateShort as CrateShortSameReach; + pub(in crate::scope_guard_visibility) use dollar_crate_ancestor::DollarCrateAncestor as DollarCrateSameReach; + use private::Private as PrivateSameReach; + pub(in super::super) use relative_ancestor::RelativeAncestor as RelativeSameReach; + use self_long::SelfLong as SelfLongSameReach; + use self_short::SelfShort as SelfShortSameReach; + pub(super) use super_long::SuperLong as SuperLongSameReach; + pub(super) use super_short::SuperShort as SuperShortSameReach; + + pub(super) fn local_surfaces() { + assert_surface(Private::positioned, Private::try_cmp_same_scope); + assert_surface( + PrivateSameReach::positioned, + PrivateSameReach::try_cmp_same_scope, + ); + assert_surface(SelfShort::positioned, SelfShort::try_cmp_same_scope); + assert_surface( + SelfShortSameReach::positioned, + SelfShortSameReach::try_cmp_same_scope, + ); + assert_surface(SelfLong::positioned, SelfLong::try_cmp_same_scope); + assert_surface( + SelfLongSameReach::positioned, + SelfLongSameReach::try_cmp_same_scope, + ); + assert_surface(SuperShort::positioned, SuperShort::try_cmp_same_scope); + assert_surface( + SuperShortSameReach::positioned, + SuperShortSameReach::try_cmp_same_scope, + ); + assert_surface(SuperLong::positioned, SuperLong::try_cmp_same_scope); + assert_surface( + SuperLongSameReach::positioned, + SuperLongSameReach::try_cmp_same_scope, + ); + assert_surface( + RelativeAncestor::positioned, + RelativeAncestor::try_cmp_same_scope, + ); + assert_surface( + RelativeSameReach::positioned, + RelativeSameReach::try_cmp_same_scope, + ); + assert_surface(CrateShort::positioned, CrateShort::try_cmp_same_scope); + assert_surface( + CrateShortSameReach::positioned, + CrateShortSameReach::try_cmp_same_scope, + ); + assert_surface(CrateLong::positioned, CrateLong::try_cmp_same_scope); + assert_surface( + CrateLongSameReach::positioned, + CrateLongSameReach::try_cmp_same_scope, + ); + assert_surface( + AbsoluteAncestor::positioned, + AbsoluteAncestor::try_cmp_same_scope, + ); + assert_surface( + AbsoluteSameReach::positioned, + AbsoluteSameReach::try_cmp_same_scope, + ); + assert_surface( + DollarCrateAncestor::positioned, + DollarCrateAncestor::try_cmp_same_scope, + ); + assert_surface( + DollarCrateSameReach::positioned, + DollarCrateSameReach::try_cmp_same_scope, + ); + } + } + + pub(super) fn ancestor_surfaces() { + inner::local_surfaces(); + assert_surface( + inner::SuperShort::positioned, + inner::SuperShort::try_cmp_same_scope, + ); + assert_surface( + inner::SuperShortSameReach::positioned, + inner::SuperShortSameReach::try_cmp_same_scope, + ); + assert_surface( + inner::SuperLong::positioned, + inner::SuperLong::try_cmp_same_scope, + ); + assert_surface( + inner::RelativeAncestor::positioned, + inner::RelativeAncestor::try_cmp_same_scope, + ); + assert_surface( + inner::AbsoluteAncestor::positioned, + inner::AbsoluteAncestor::try_cmp_same_scope, + ); + assert_surface( + inner::CrateShort::positioned, + inner::CrateShort::try_cmp_same_scope, + ); + assert_surface( + inner::CrateLong::positioned, + inner::CrateLong::try_cmp_same_scope, + ); + } + } + + pub(super) fn ancestor_surfaces() { + shallow::local_surfaces(); + assert_surface( + shallow::SuperShort::positioned, + shallow::SuperShort::try_cmp_same_scope, + ); + assert_surface( + shallow::SuperLong::positioned, + shallow::SuperLong::try_cmp_same_scope, + ); + assert_surface( + shallow::AbsoluteAncestor::positioned, + shallow::AbsoluteAncestor::try_cmp_same_scope, + ); + assert_surface( + shallow::DollarCrateAncestor::positioned, + shallow::DollarCrateAncestor::try_cmp_same_scope, + ); + assert_surface( + shallow::CrateShort::positioned, + shallow::CrateShort::try_cmp_same_scope, + ); + assert_surface( + shallow::CrateLong::positioned, + shallow::CrateLong::try_cmp_same_scope, + ); + + deep::ancestor_surfaces(); + assert_surface( + deep::inner::RelativeAncestor::positioned, + deep::inner::RelativeAncestor::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DollarCrateAncestor::positioned, + deep::inner::DollarCrateAncestor::try_cmp_same_scope, + ); + assert_surface( + deep::inner::CrateShort::positioned, + deep::inner::CrateShort::try_cmp_same_scope, + ); + assert_surface( + deep::inner::CrateLong::positioned, + deep::inner::CrateLong::try_cmp_same_scope, + ); + } +} + +/// The admitted visibility grammar preserves its caller coordinate at two +/// nesting depths, including shorthand/equivalent forms, relative chains, +/// absolute `crate` paths, macro-authored `$crate` paths, and public reach. +/// +/// green: identity.scope-guard-visibility-is-caller-relative +#[test] +fn every_scope_guard_visibility_form_keeps_its_caller_coordinate() { + scope_guard_visibility::ancestor_surfaces(); + assert_surface( + scope_guard_visibility::shallow::CrateShort::positioned, + scope_guard_visibility::shallow::CrateShort::try_cmp_same_scope, + ); + assert_surface( + scope_guard_visibility::shallow::CrateLong::positioned, + scope_guard_visibility::shallow::CrateLong::try_cmp_same_scope, + ); + assert_surface( + scope_guard_visibility::deep::inner::CrateShort::positioned, + scope_guard_visibility::deep::inner::CrateShort::try_cmp_same_scope, + ); + assert_surface( + scope_guard_visibility::deep::inner::CrateLong::positioned, + scope_guard_visibility::deep::inner::CrateLong::try_cmp_same_scope, + ); +} diff --git a/xtask/fixtures/macro-consumer/src/lib.rs b/xtask/fixtures/macro-consumer/src/lib.rs index 1236d38..902757e 100644 --- a/xtask/fixtures/macro-consumer/src/lib.rs +++ b/xtask/fixtures/macro-consumer/src/lib.rs @@ -9,12 +9,456 @@ //! //! This crate answers it from outside. It depends on `threadpak` and on //! `threadpak-macros`, exactly as an application would, and on neither of their -//! internals. It exports nothing: the whole crate is the fixture, and its tests -//! are the proof. +//! internals. Its public scope-guard specimen is intentional: rustdoc compiles +//! its examples as a crate outside this fixture, which proves the public +//! re-export crosses a real crate boundary while the narrower forms do not. //! //! It lives under `xtask/fixtures/` because it is tooling — first-class, never //! on the production dependency path. +/// The complete visibility grammar of `scope_guard_version!`, exercised at two +/// nesting depths by a crate outside `threadpak`. +/// +/// The public forms cross this fixture's crate boundary with both emitted +/// operations intact: +/// +/// ``` +/// use threadpak::identity::{AuthorityPosition, OrderComparison}; +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::{ +/// ShallowPublic, ShallowScope, +/// }; +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::{ +/// DeepPublic, DeepScope, +/// }; +/// +/// let _positioned: fn(AuthorityPosition) -> ShallowPublic = +/// ShallowPublic::positioned; +/// let _comparison: fn(&ShallowPublic, &ShallowPublic) -> Result< +/// core::cmp::Ordering, +/// OrderComparison, +/// > = ShallowPublic::try_cmp_same_scope; +/// let _deep_positioned: fn(AuthorityPosition) -> DeepPublic = +/// DeepPublic::positioned; +/// let _deep_comparison: fn(&DeepPublic, &DeepPublic) -> Result< +/// core::cmp::Ordering, +/// OrderComparison, +/// > = DeepPublic::try_cmp_same_scope; +/// ``` +/// +/// Every narrower shallow form stops at this crate boundary independently: +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowPrivate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowSelf; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowInSelf; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowInSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowInSuperSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowCrate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowInCrate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowInAncestor; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::shallow::ShallowDollarCrate; +/// ``` +/// +/// The same independent refusals hold one module deeper: +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepPrivate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepSelf; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepInSelf; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepInSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepInSuperSuper; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepCrate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepInCrate; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepInAncestor; +/// ``` +/// +/// ```compile_fail +/// use threadpak_macro_consumer::scope_guard_visibility::deep::inner::DeepDollarCrate; +/// ``` +pub mod scope_guard_visibility { + use threadpak::identity::{AuthorityPosition, OrderComparison}; + + macro_rules! crate_relative_scope_guard { + ($name:ident, $scope:ty, $home:ident) => { + threadpak::scope_guard_version! { + /// A guard whose absolute visibility root was minted by an outer macro. + pub(in $crate::scope_guard_visibility) struct $name over $scope, + seated in mod $home; + } + }; + } + + /// Ask the compiler to coerce both emitted operations to their exact public + /// signatures. Calling this helper needs no runtime position mint. + pub(crate) fn assert_surface( + _: fn(AuthorityPosition) -> Guard, + _: fn(&Guard, &Guard) -> Result, + ) { + } + + /// One-module-deep invocations covering every admitted visibility form. + pub mod shallow { + use super::assert_surface; + + /// The scope shared by the shallow visibility specimens. + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct ShallowScope; + + threadpak::scope_guard_version! { + /// A guard private at its invocation coordinate. + struct ShallowPrivate over ShallowScope, seated in mod shallow_private; + } + + threadpak::scope_guard_version! { + /// A guard explicitly private at its invocation coordinate. + pub(self) struct ShallowSelf over ShallowScope, seated in mod shallow_self; + } + + threadpak::scope_guard_version! { + /// The long spelling of invocation-coordinate privacy. + pub(in self) struct ShallowInSelf over ShallowScope, seated in mod shallow_in_self; + } + + threadpak::scope_guard_version! { + /// A guard visible to the invocation module's parent. + pub(super) struct ShallowSuper over ShallowScope, seated in mod shallow_super; + } + + threadpak::scope_guard_version! { + /// The long spelling of parent visibility. + pub(in super) struct ShallowInSuper over ShallowScope, + seated in mod shallow_in_super; + } + + threadpak::scope_guard_version! { + /// A guard visible through two relative ancestor steps. + pub(in super::super) struct ShallowInSuperSuper over ShallowScope, + seated in mod shallow_in_super_super; + } + + threadpak::scope_guard_version! { + /// A guard visible throughout this fixture crate. + pub(crate) struct ShallowCrate over ShallowScope, seated in mod shallow_crate; + } + + threadpak::scope_guard_version! { + /// The long spelling of crate visibility. + pub(in crate) struct ShallowInCrate over ShallowScope, + seated in mod shallow_in_crate; + } + + threadpak::scope_guard_version! { + /// A guard visible in the named ancestor module. + pub(in crate::scope_guard_visibility) struct ShallowInAncestor over ShallowScope, + seated in mod shallow_in_ancestor; + } + + crate_relative_scope_guard!(ShallowDollarCrate, ShallowScope, shallow_dollar_crate); + + threadpak::scope_guard_version! { + /// A guard exported from this downstream fixture. + pub struct ShallowPublic over ShallowScope, seated in mod shallow_public; + } + + /// Proves every shallow guard and both operations are reachable inside + /// the invocation module. + pub(super) fn local_surfaces_are_reachable() { + assert_surface( + ShallowPrivate::positioned, + ShallowPrivate::try_cmp_same_scope, + ); + assert_surface(ShallowSelf::positioned, ShallowSelf::try_cmp_same_scope); + assert_surface(ShallowInSelf::positioned, ShallowInSelf::try_cmp_same_scope); + assert_surface(ShallowSuper::positioned, ShallowSuper::try_cmp_same_scope); + assert_surface( + ShallowInSuper::positioned, + ShallowInSuper::try_cmp_same_scope, + ); + assert_surface( + ShallowInSuperSuper::positioned, + ShallowInSuperSuper::try_cmp_same_scope, + ); + assert_surface(ShallowCrate::positioned, ShallowCrate::try_cmp_same_scope); + assert_surface( + ShallowInCrate::positioned, + ShallowInCrate::try_cmp_same_scope, + ); + assert_surface( + ShallowInAncestor::positioned, + ShallowInAncestor::try_cmp_same_scope, + ); + assert_surface( + ShallowDollarCrate::positioned, + ShallowDollarCrate::try_cmp_same_scope, + ); + assert_surface(ShallowPublic::positioned, ShallowPublic::try_cmp_same_scope); + } + } + + /// Two-module-deep invocations covering every admitted visibility form. + pub mod deep { + use super::assert_surface; + + /// The invocation module at the second nesting depth. + pub mod inner { + use super::assert_surface; + + /// The scope shared by the deep visibility specimens. + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct DeepScope; + + threadpak::scope_guard_version! { + /// A guard private at its invocation coordinate. + struct DeepPrivate over DeepScope, seated in mod deep_private; + } + + threadpak::scope_guard_version! { + /// A guard explicitly private at its invocation coordinate. + pub(self) struct DeepSelf over DeepScope, seated in mod deep_self; + } + + threadpak::scope_guard_version! { + /// The long spelling of invocation-coordinate privacy. + pub(in self) struct DeepInSelf over DeepScope, seated in mod deep_in_self; + } + + threadpak::scope_guard_version! { + /// A guard visible to the invocation module's parent. + pub(super) struct DeepSuper over DeepScope, seated in mod deep_super; + } + + threadpak::scope_guard_version! { + /// The long spelling of parent visibility. + pub(in super) struct DeepInSuper over DeepScope, seated in mod deep_in_super; + } + + threadpak::scope_guard_version! { + /// A guard visible through two relative ancestor steps. + pub(in super::super) struct DeepInSuperSuper over DeepScope, + seated in mod deep_in_super_super; + } + + threadpak::scope_guard_version! { + /// A guard visible throughout this fixture crate. + pub(crate) struct DeepCrate over DeepScope, seated in mod deep_crate; + } + + threadpak::scope_guard_version! { + /// The long spelling of crate visibility. + pub(in crate) struct DeepInCrate over DeepScope, seated in mod deep_in_crate; + } + + threadpak::scope_guard_version! { + /// A guard visible in the named ancestor module. + pub(in crate::scope_guard_visibility::deep) struct DeepInAncestor over DeepScope, + seated in mod deep_in_ancestor; + } + + crate_relative_scope_guard!(DeepDollarCrate, DeepScope, deep_dollar_crate); + + threadpak::scope_guard_version! { + /// A guard exported from this downstream fixture. + pub struct DeepPublic over DeepScope, seated in mod deep_public; + } + + /// Proves every deep guard and both operations are reachable inside + /// the invocation module. + pub(super) fn local_surfaces_are_reachable() { + assert_surface(DeepPrivate::positioned, DeepPrivate::try_cmp_same_scope); + assert_surface(DeepSelf::positioned, DeepSelf::try_cmp_same_scope); + assert_surface(DeepInSelf::positioned, DeepInSelf::try_cmp_same_scope); + assert_surface(DeepSuper::positioned, DeepSuper::try_cmp_same_scope); + assert_surface(DeepInSuper::positioned, DeepInSuper::try_cmp_same_scope); + assert_surface( + DeepInSuperSuper::positioned, + DeepInSuperSuper::try_cmp_same_scope, + ); + assert_surface(DeepCrate::positioned, DeepCrate::try_cmp_same_scope); + assert_surface(DeepInCrate::positioned, DeepInCrate::try_cmp_same_scope); + assert_surface( + DeepInAncestor::positioned, + DeepInAncestor::try_cmp_same_scope, + ); + assert_surface( + DeepDollarCrate::positioned, + DeepDollarCrate::try_cmp_same_scope, + ); + assert_surface(DeepPublic::positioned, DeepPublic::try_cmp_same_scope); + } + } + + /// Proves the parent- and named-ancestor forms, plus the wider forms, + /// remain reachable at the deep invocation's parent. + pub(super) fn ancestor_surfaces_are_reachable() { + inner::local_surfaces_are_reachable(); + assert_surface( + inner::DeepSuper::positioned, + inner::DeepSuper::try_cmp_same_scope, + ); + assert_surface( + inner::DeepInSuper::positioned, + inner::DeepInSuper::try_cmp_same_scope, + ); + assert_surface( + inner::DeepInSuperSuper::positioned, + inner::DeepInSuperSuper::try_cmp_same_scope, + ); + assert_surface( + inner::DeepInAncestor::positioned, + inner::DeepInAncestor::try_cmp_same_scope, + ); + assert_surface( + inner::DeepCrate::positioned, + inner::DeepCrate::try_cmp_same_scope, + ); + assert_surface( + inner::DeepInCrate::positioned, + inner::DeepInCrate::try_cmp_same_scope, + ); + assert_surface( + inner::DeepPublic::positioned, + inner::DeepPublic::try_cmp_same_scope, + ); + } + } + + /// Proves every form reachable in this common ancestor keeps both emitted + /// operations there. + pub fn ancestor_surfaces_are_reachable() { + shallow::local_surfaces_are_reachable(); + assert_surface( + shallow::ShallowSuper::positioned, + shallow::ShallowSuper::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowInSuper::positioned, + shallow::ShallowInSuper::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowInSuperSuper::positioned, + shallow::ShallowInSuperSuper::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowCrate::positioned, + shallow::ShallowCrate::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowInCrate::positioned, + shallow::ShallowInCrate::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowInAncestor::positioned, + shallow::ShallowInAncestor::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowDollarCrate::positioned, + shallow::ShallowDollarCrate::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowPublic::positioned, + shallow::ShallowPublic::try_cmp_same_scope, + ); + + deep::ancestor_surfaces_are_reachable(); + assert_surface( + deep::inner::DeepCrate::positioned, + deep::inner::DeepCrate::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DeepInCrate::positioned, + deep::inner::DeepInCrate::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DeepDollarCrate::positioned, + deep::inner::DeepDollarCrate::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DeepPublic::positioned, + deep::inner::DeepPublic::try_cmp_same_scope, + ); + } +} + +/// Proves the two `pub(crate)` forms and both public forms are reachable from +/// this downstream fixture's crate root, outside either invocation ancestry. +pub fn scope_guard_crate_surfaces_are_reachable() { + use scope_guard_visibility::{assert_surface, deep, shallow}; + + assert_surface( + shallow::ShallowCrate::positioned, + shallow::ShallowCrate::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowInSuperSuper::positioned, + shallow::ShallowInSuperSuper::try_cmp_same_scope, + ); + assert_surface( + shallow::ShallowPublic::positioned, + shallow::ShallowPublic::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DeepCrate::positioned, + deep::inner::DeepCrate::try_cmp_same_scope, + ); + assert_surface( + deep::inner::DeepPublic::positioned, + deep::inner::DeepPublic::try_cmp_same_scope, + ); +} + #[cfg(test)] mod tests { use threadpak::refusal::{ @@ -134,6 +578,15 @@ mod tests { ); } + /// The caller-relative visibility grammar compiles in an outside consumer; + /// both public forms are checked again by rustdoc from one crate farther + /// out. The routed positive control lives in testpak's dedicated matrix. + #[test] + fn every_scope_guard_visibility_form_keeps_its_caller_coordinate() { + crate::scope_guard_visibility::ancestor_surfaces_are_reachable(); + crate::scope_guard_crate_surfaces_are_reachable(); + } + /// The values themselves still behave like an ordinary Rust enum: the /// derive added declared facts and took nothing away. #[test] From 12dca235745981333bdb5658967d03fa5ac3a19c Mon Sep 17 00:00:00 2001 From: Heyoub Date: Sat, 15 Aug 2026 20:14:30 -0400 Subject: [PATCH 4/5] Contain Scratch Git routing --- xtask/src/checks/scratch.rs | 63 +++++++++++++++++++++++++++++--- xtask/src/repository/snapshot.rs | 12 ++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/xtask/src/checks/scratch.rs b/xtask/src/checks/scratch.rs index 97d1fc7..9d111ba 100644 --- a/xtask/src/checks/scratch.rs +++ b/xtask/src/checks/scratch.rs @@ -9,16 +9,19 @@ //! never proven by dirtying the tree. Fixture planting and deliberate removal //! carry every filesystem refusal; drop makes a best-effort cleanup only after //! the fixture can no longer affect a verdict. +//! Scratch Git operations use the repository reader's command constructor, so +//! the explicit fixture root receives the same complete ambient-routing +//! containment as the production committed snapshot. //! //! This module exists only under `cfg(test)`: it is fixture machinery shared by //! several law families, and it ships in no binary. use std::fs; use std::path::PathBuf; -use std::process::{Command, Stdio}; +use std::process::Stdio; use std::sync::atomic::{AtomicUsize, Ordering}; -use crate::repository::snapshot::RepositorySnapshot; +use crate::repository::snapshot::{RepositorySnapshot, git as repository_git}; /// One scratch root outside the repository, and the files planted in it. pub(crate) struct Scratch { @@ -103,9 +106,7 @@ impl Scratch { /// Runs one Git operation against this fixture and carries its refusal. fn git(&self, arguments: &[&str]) -> Result<(), String> { - let output = Command::new("git") - .current_dir(&self.root) - .env("GIT_NO_REPLACE_OBJECTS", "1") + let output = repository_git(&self.root) .args(arguments) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -134,7 +135,59 @@ impl Drop for Scratch { #[cfg(test)] mod tests { + use std::process::{Command, Stdio}; + use super::Scratch; + use crate::repository::snapshot::RepositorySnapshot; + + /// Git routing inherited by the test process cannot redirect the fixture's + /// initialization, index, or commit away from the explicit scratch root. + /// + /// The hostile is injected only into a child process. No process-global + /// environment is changed while the rest of the Rust tests may be running. + #[test] + fn ambient_git_routing_cannot_redirect_scratch_commits() -> Result<(), String> { + const CHILD: &str = "THREADPAK_SCRATCH_GIT_ROUTING_CHILD"; + + if std::env::var_os(CHILD).is_some() { + let scratch = Scratch::named("routing-explicit")?; + scratch.write("explicit.txt", "explicit scratch root\n")?; + let snapshot = scratch.read()?; + assert!(snapshot.files().get("explicit.txt").is_some()); + assert!(snapshot.files().get("alternate.txt").is_none()); + return Ok(()); + } + + let alternate = Scratch::named("routing-alternate")?; + alternate.write("alternate.txt", "alternate repository\n")?; + let before = alternate.read()?.committed().to_string(); + + let child = std::env::current_exe() + .map_err(|error| format!("current xtask test executable: {error}"))?; + let output = Command::new(child) + .arg("ambient_git_routing_cannot_redirect_scratch_commits") + .arg("--test-threads=1") + .env(CHILD, "1") + .env("GIT_DIR", alternate.root.join(".git")) + .env("GIT_WORK_TREE", &alternate.root) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("scratch-routing child test: {error}"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + if !output.status.success() || !stdout.contains("1 passed") { + return Err(format!( + "scratch-routing child did not commit the explicit root:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr) + )); + } + + let after = RepositorySnapshot::read(&alternate.root)? + .committed() + .to_string(); + assert_eq!(after, before, "Scratch Git mutated the routed repository"); + Ok(()) + } /// A failed removal is an explicit fixture failure, not a successful /// absence plant that lets the challenged law run against the wrong tree. diff --git a/xtask/src/repository/snapshot.rs b/xtask/src/repository/snapshot.rs index 5ccc4bf..84dbc39 100644 --- a/xtask/src/repository/snapshot.rs +++ b/xtask/src/repository/snapshot.rs @@ -691,11 +691,17 @@ fn revision(root: &Path, spelling: &str) -> Result { /// /// Environment routing is removed, repository-local `core.worktree` is /// overridden by the command-line coordinate, and replacement-object rewriting -/// is disabled. Git still discovers the matching `.git` directory or gitfile at -/// `root`, which preserves ordinary clones and linked worktrees alike. -fn git(root: &Path) -> Command { +/// is disabled. Existing clones and linked worktrees still discover the +/// matching `.git` directory or gitfile at `root`. Before that coordinate +/// exists, the command names the exact storage path as well, so a caller such as +/// the scratch fixture can initialize this root without discovering a parent or +/// inheriting another repository. +pub(crate) fn git(root: &Path) -> Command { let mut command = Command::new("git"); command.current_dir(root).arg("--work-tree").arg(root); + if !root.join(GIT_STORAGE).exists() { + command.arg("--git-dir").arg(root.join(GIT_STORAGE)); + } for variable in GIT_ROUTING_ENVIRONMENT { command.env_remove(variable); } From b74828851e926bda85a630430036c2e1f41f8ce8 Mon Sep 17 00:00:00 2001 From: Heyoub Date: Sat, 15 Aug 2026 20:22:56 -0400 Subject: [PATCH 5/5] Clarify scope guard evidence seat --- testpak/tests/scope_guard_visibility.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/testpak/tests/scope_guard_visibility.rs b/testpak/tests/scope_guard_visibility.rs index 15305fd..e23ac81 100644 --- a/testpak/tests/scope_guard_visibility.rs +++ b/testpak/tests/scope_guard_visibility.rs @@ -330,13 +330,14 @@ mod scope_guard_visibility { } } -/// The admitted visibility grammar preserves its caller coordinate at two -/// nesting depths, including shorthand/equivalent forms, relative chains, -/// absolute `crate` paths, macro-authored `$crate` paths, and public reach. +/// The admitted narrow visibility grammar preserves its caller coordinate at +/// two nesting depths, including shorthand/equivalent forms, relative chains, +/// absolute `crate` paths, and macro-authored `$crate` paths. The downstream +/// `pub` control lives in `xtask/fixtures/macro-consumer/src/lib.rs`. /// /// green: identity.scope-guard-visibility-is-caller-relative #[test] -fn every_scope_guard_visibility_form_keeps_its_caller_coordinate() { +fn every_narrow_scope_guard_visibility_form_keeps_its_caller_coordinate() { scope_guard_visibility::ancestor_surfaces(); assert_surface( scope_guard_visibility::shallow::CrateShort::positioned,