Review the current branch's changes against master (or another base if specified).
Produce a structured review covering correctness, architecture, tests, and
repo-specific concerns. Adapt depth to the PR type — migration PRs get extra
migration-checklist items; feature/fix PRs skip migration sections.
Step 1 — Orient
git fetch origin master # local master may be stale
git log --oneline origin/master..HEAD # commits in this PR
git diff --stat origin/master...HEAD # files changed
git diff origin/master...HEAD # full diff (read selectively)
Use the three-dot range for git diff: it diffs against the merge base, so commits landed
on master after the branch was cut don't show up as spurious reversions in the PR. (git log
keeps the two-dot form — for log it already means "reachable from HEAD, not from master".)
Identify the PR type:
- ObjC→Swift migration — files under
PrebidMobile/Objc/deleted and matching files underPrebidMobile/Swift/...added - Feature / bug fix — changes to existing Swift or ObjC logic
- Tooling / CI — script or config changes
- Docs only —
.mdfiles only
Step 2 — Run the standard checks
# Build all XCFrameworks (catches CocoaPods + SPM + Carthage regressions)
./scripts/buildPrebidMobile.sh
# PR test suite (must pass; re-run once if only PBMBidRequesterTest.testBanner_300x250 fails)
./scripts/testPrebidMobile.sh --latest --quick
# SwiftLint (warnings are ok; errors block merge)
swiftlint --config .swiftlint.yml
If the full suite is warranted (final PR in a phase, or touching networking):
./scripts/testPrebidMobile.sh --latest
Step 3 — Code review
For any PR
Correctness
- Logic matches the intent described in the commit message / PR description
- No obvious off-by-one, nil-crash, or force-unwrap in production paths
- Error paths are handled; no silent swallowing of errors
- Thread safety: any shared mutable state accessed from multiple queues?
Swift quality
- No single-letter variable names (
d,n,e, etc.) - No unnecessary
@objcon internal-only Swift code publicaccess only where needed; preferinternal- Prefer
letovervarwhere value never changes
iOS SDK specifics
- No changes to public API surface without explicit intent (check
PrebidMobile/Swift/public types) fetchDemandcall sites and ad unit lifecycle unaffected- Min deployment target stays iOS 13.0 — no API calls that require higher
- No new ObjC dependency added to the Swift layer without justification
- Never log consent strings, IDFA, or EIDs (
ORTBUser.ext) — this SDK handles all three
Ad rendering — the SDK draws into someone else's window
- SDK-drawn controls (close, skip, mute, Learn More) keep a ≥44×44pt tap target. Note
AdViewButtonDecorator.getButtonSize()computes0.1 × screenWidth, which is under 44pt on most of the device fleet — don't propagate that pattern. - No
UIScreen.main.boundsfor sizing or positioning. The host app's window may be a fraction of the screen under Split View, Slide Over, or Stage Manager; size from the containing view's bounds. Likewise avoidUIApplication.statusBarOrientation(deprecated since iOS 13) — read the trait environment instead. - Overlay chrome composited over arbitrary advertiser content meets contrast floors: 4.5:1 for text, 3:1 for UI components. A translucent scrim over unknown video does not guarantee either.
- Text the SDK draws either scales with Dynamic Type (
UIFontMetrics.scaledFont(for:)) or is deliberately capped for a fixed-height overlay — not silently frozen at a point size. - When ad chrome changes, spot-check with VoiceOver on, text at Accessibility XXXL, and Reduce Transparency enabled.
Tests
- New tests run in the PR plan.
PrebidMobilePRTests.xctestplanselects by exclusion (skippedTests), so new classes are picked up automatically — only verify the class isn't listed there. - Existing tests not weakened. In particular,
assertForOverFulfill = false,XCTAssert(true), and assertions relaxed to!error.localizedDescription.isEmptyhide the failure instead of fixing it; prefer isolating the test (e.g. tagging fixtures per test instance so a shared singleton's callbacks can be filtered). - No mocked database / network when a real one is feasible
For ObjC→Swift migration PRs (additional checks)
File structure
- Swift twins land under
PrebidMobile/Swift/PrebidMobileRendering/ORTB/Request/(or mirrored path) - Filename:
ORTBFoo.swift(noPBMprefix) - Class declaration:
@objc(PBMORTBFoo) public class ORTBFoo: NSObject, PBMJsonCodable
ObjC bridge completeness (Gaps 6 & 7)
- Class is
@objc public class— not justpublic class - All
@objc-visible properties have@objc public var - Init:
@objc(initWithJsonDictionary:) public required init(jsonDictionary:)— non-optional, callssuper.init()first - Encode:
@objc(toJsonDictionary) public var jsonDictionary: [String: Any]
JSON parity
- Every ObjC property has a corresponding Swift property with the exact same JSON key string
- Empty arrays are included as-is (not suppressed) —
pbmCopyWithoutEmptyValsonly strips nil/NSNull (Gap 9) - Child ORTB objects use
json[.key] = childObj— empty-dict suppression is automatic (Gap 2) lat/lononORTBGeomust useNSDecimalNumber(decimal:)on encode to preserve decimal precision
NSMutableDictionary properties (Gap discovered S1.4)
- Any property typed
NSMutableDictionary *in ObjC must be decoded as:NSMutableDictionary(dictionary: extDict)— NOTas? NSMutableDictionary(silently returns nil)
NSCopying (Gap discovered S1.4)
- If the ObjC type conformed to
<NSCopying>, the Swift twin must also conform - Implement via JSON round-trip:
Self(jsonDictionary: jsonDictionary)
JSONObject.dict private(set) (Gap 10)
- Cannot write
json.dict["ext"] = valfrom outside the struct - Use
var result = json.dict; result["ext"] = val; return resultfor untyped sub-dict injection
ObjC file cleanup
.mfile deleted ✓PrivateHeaders/*.hdeleted ✓PBMORTBAbstract+Protected.himport removed from the deleted.m(not from Phase 3/4 consumers)PBMORTB.humbrella updated (deleted type removed from imports)- Remaining ObjC consumers updated:
#import "PBMORTBFoo.h"→#import "SwiftImport.h" - Xcode project:
.m/.hrefs removed,.swiftref added (checkproject.pbxprojdiff)
Test file updates
- Swift test files use new
ORTBFoonames (notPBMORTBFoo) — no'has been renamed'compiler errors - Used
perl -pi -e 's/PBMORTBFoo\b/ORTBFoo/g'(notsed, which has unreliable\bon macOS) codeAndDecode<T: PBMORTBAbstract>overload removed oncePBMORTBAbstract.mis deleted- Decode coverage includes a partial JSON payload, not only a fully-populated fixture — round-tripping a full fixture cannot catch a resurrected class default (Gap S2.5-C)
Docs
docs/migration/playbook.mdupdated if a new gap was discovered- The PR's
docs/migration/pr-phase-*.mdupdated with step summary and ticked test-plan boxes, and no superseded draft docs left alongside it
Step 4 — Known flaky test
PBMBidRequesterTest.testBanner_300x250 fails intermittently under full-suite
simulator load but passes in isolation. Do NOT flag this as a regression unless
it also fails when run alone:
xcodebuild ... -only-testing PrebidMobileTests/PBMBidRequesterTest/testBanner_300x250 \
test-without-building
Step 5 — Produce the review
Structure the output as:
## Summary
One paragraph: what the PR does, and the overall verdict (approve / request changes / comment).
## Blockers
Numbered list of must-fix items (empty if none).
## Suggestions
Numbered list of non-blocking improvements.
## Migration checklist (migration PRs only)
[ ] ObjC bridge annotations present
[ ] JSON key strings match ObjC originals
[ ] ObjC files deleted, consumers patched, project.pbxproj clean
[ ] Test files renamed, new classes registered in PRTests plan
[ ] Playbook updated if new gap discovered
[ ] Build + quick tests green
## Nits
One-liners: style, naming, comment quality. Low priority.
Keep the review concise. Lead with blockers. Skip sections that have nothing to report.