Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0742533
Add Firebase binding audit tooling
AdamEssenmacher Apr 13, 2026
b41f032
Fix Sharpie companion import resolution
AdamEssenmacher Apr 13, 2026
27098e1
Address audit review fallback handling
AdamEssenmacher Apr 13, 2026
6255435
Address audit review and AppDistribution normalization
AdamEssenmacher Apr 13, 2026
a509e43
Merge remote-tracking branch 'origin/main' into codex/firebase-audit-…
AdamEssenmacher Apr 13, 2026
336850e
Address audit review comments
AdamEssenmacher Apr 13, 2026
91f2ee4
Add Firebase binding surface coverage E2E lane
AdamEssenmacher Apr 13, 2026
9c2a866
Match manual coverage surfaces by signature
AdamEssenmacher Apr 13, 2026
e26ba90
Match bound coverage overloads by signature
AdamEssenmacher Apr 13, 2026
4e8c4c2
Validate coverage surface return types
AdamEssenmacher Apr 13, 2026
abf8897
Cover AppDistribution binding surface
AdamEssenmacher Apr 13, 2026
9454b8d
Tighten binding surface verifier matching
AdamEssenmacher Apr 13, 2026
cc47d5a
Address audit matcher review comments
AdamEssenmacher Apr 13, 2026
8efd9a4
Tighten binding surface member matching
AdamEssenmacher Apr 13, 2026
fc89f74
Preserve coverage surface metadata
AdamEssenmacher Apr 13, 2026
c3c1ef7
Preserve helper parameter modifiers in coverage
AdamEssenmacher Apr 13, 2026
d0db07d
Tighten binding surface coverage matching
AdamEssenmacher Apr 13, 2026
32b48de
Preserve manual surface member kinds
AdamEssenmacher Apr 13, 2026
754479d
Cover manual export selectors and delegates
AdamEssenmacher Apr 13, 2026
dfcfdb5
Validate binding surface type shapes
AdamEssenmacher Apr 14, 2026
3513ff1
Validate manual symbol wrapper surfaces
AdamEssenmacher Apr 14, 2026
7e61eaf
Preserve manual surface container kinds
AdamEssenmacher Apr 14, 2026
12e348b
Document AppDistribution coverage packaging
AdamEssenmacher Apr 14, 2026
9b22b92
Merge pull request #142 from AdamEssenmacher/codex/firebase-binding-s…
AdamEssenmacher Apr 14, 2026
c44f271
Address audit PR review feedback
AdamEssenmacher Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/firebase-binding-api-shape-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Firebase Binding API Shape Policy

This note records the comparison policy used when triaging Firebase binding drift. The goal is to keep bindings thin and runtime-correct while avoiding unnecessary breaking changes for APIs that are already in use.

## Preferred Shape

- Selector correctness wins. If a binding has the wrong selector, wrong native argument type, or another mismatch that can throw before Firebase business logic runs, fix the binding directly.
- Prefer `Action<>` callback smoothing for new or additive APIs that bind Objective-C block callbacks.
- Do not replace existing named-delegate callback APIs just because `Action<>` is preferred. Keep the existing API for compatibility, then add the preferred `Action<>` shape case-by-case when it is useful and can be validated.
- Obsolete non-preferred callback shapes only after the preferred shape exists and the migration path is clear.
- Keep bindings thin. Avoid adding managed behavior in `ApiDefinition.cs` just to paper over native API shape differences.

## Audit Treatment

- Treat read-only properties and zero-argument getter methods as equivalent when they bind the same native selector and have equivalent return types.
- Treat raw and typed Foundation collection wrappers, such as `NSDictionary` and `NSDictionary<NSString, NSObject>`, as equivalent for binding-surface comparison when the selector is otherwise the same.
- Treat named-delegate callbacks and equivalent `Action<>` callbacks as compatible, but report them as informational preferred-shape candidates so they can be reviewed case-by-case.
- Do not normalize semantic delegate aliases that are not mechanically equivalent. Those should stay visible until header/source review proves the mapping.
- Do not churn enum underlying types or `nint`/`long`-style numeric differences without a concrete runtime or interoperability reason.

## Case-By-Case Obsoletion Flow

1. Confirm the native header shape and the current managed shape.
2. Add the preferred binding shape only when it improves runtime correctness or developer experience without hiding native semantics.
3. Add or update a targeted E2E case when the API can be exercised locally.
4. Keep the existing non-preferred API unless it was already runtime-broken.
5. Mark the older API obsolete only when the preferred replacement is present and the compatibility tradeoff is intentional.
197 changes: 197 additions & 0 deletions scripts/FirebaseBindingAudit.Tests/AuditRunnerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using Xunit;

namespace FirebaseBindingAudit.Tests;

public sealed class AuditRunnerTests
{
[Fact]
public void ShouldUseSharpieComparisonFallback_UsesForceFallbackWhenPrimaryComparisonSucceeded()
{
var target = new AuditTargetDefinition
{
UseSharpieComparisonFallback = true
};

var sharpieRun = new SharpieRunResult(
Status: "succeeded",
FrameworkPath: null,
ProcessResult: null,
Comparison: new TargetComparisonResult([], []),
Snapshot: null,
GeneratedFiles: [],
WarningFindings: [],
Message: null);

var shouldFallback = AuditRunner.ShouldUseSharpieComparisonFallback(
target,
sharpieMode: "forceFallback",
primaryFailed: false,
primaryComparableSurfaceEmpty: false,
sharpieRun);

Assert.True(shouldFallback);
}

[Fact]
public void ShouldUseSharpieComparisonFallback_DoesNotUseAutoWhenPrimaryComparisonSucceeded()
{
var target = new AuditTargetDefinition
{
UseSharpieComparisonFallback = true
};

var sharpieRun = new SharpieRunResult(
Status: "succeeded",
FrameworkPath: null,
ProcessResult: null,
Comparison: new TargetComparisonResult([], []),
Snapshot: null,
GeneratedFiles: [],
WarningFindings: [],
Message: null);

var shouldFallback = AuditRunner.ShouldUseSharpieComparisonFallback(
target,
sharpieMode: "auto",
primaryFailed: false,
primaryComparableSurfaceEmpty: false,
sharpieRun);

Assert.False(shouldFallback);
}

[Fact]
public void ShouldStageSharpieFrameworkSlices_SkipsWhenSharpieIsDisabled()
{
var selectedTargets = new[] { CreateTarget("Auth", "FirebaseAuth") };

Assert.False(AuditRunner.ShouldStageSharpieFrameworkSlices(
CreateAuditOptions(disableSharpie: true),
selectedTargets,
CreateSharpieConfiguration()));
Assert.True(AuditRunner.ShouldStageSharpieFrameworkSlices(
CreateAuditOptions(disableSharpie: false),
selectedTargets,
CreateSharpieConfiguration()));
}

[Fact]
public void ShouldStageSharpieFrameworkSlices_SkipsTargetsWithSharpieOff()
{
var selectedTargets = new[] { CreateTarget("Auth", "FirebaseAuth", sharpieMode: "off") };

var shouldStage = AuditRunner.ShouldStageSharpieFrameworkSlices(
CreateAuditOptions(disableSharpie: false),
selectedTargets,
CreateSharpieConfiguration());

Assert.False(shouldStage);
}

[Fact]
public void ShouldStageSharpieFrameworkSlices_IncludesAutoAndForceFallbackTargets()
{
var selectedTargets = new[]
{
CreateTarget("Auth", "FirebaseAuth", sharpieMode: "auto"),
CreateTarget("CloudFirestore", "FirebaseFirestore", sharpieMode: "forceFallback")
};

var stagedXcframeworks = AuditRunner.GetSharpieFrameworkSliceStageXcframeworks(
CreateAuditOptions(disableSharpie: false),
selectedTargets,
CreateSharpieConfiguration());

Assert.Equal(["FirebaseAuth", "FirebaseFirestore"], stagedXcframeworks);
}

[Fact]
public void GetSharpieFrameworkSliceStageXcframeworks_ExcludesUnselectedTargets()
{
var selectedTargets = new[] { CreateTarget("Auth", "FirebaseAuth") };

var stagedXcframeworks = AuditRunner.GetSharpieFrameworkSliceStageXcframeworks(
CreateAuditOptions(disableSharpie: false),
selectedTargets,
CreateSharpieConfiguration());

Assert.Equal(["FirebaseAuth"], stagedXcframeworks);
Assert.DoesNotContain("UnsupportedUnselectedFramework", stagedXcframeworks);
}

[Fact]
public void CreateTempRootPath_ReturnsUniqueAuditWorkspacePaths()
{
var firstPath = AuditRunner.CreateTempRootPath();
var secondPath = AuditRunner.CreateTempRootPath();

Assert.NotEqual(firstPath, secondPath);
Assert.EndsWith("firebase-binding-audit", Directory.GetParent(firstPath)!.Name);
Assert.Contains("-", Path.GetFileName(firstPath), StringComparison.Ordinal);
}

[Fact]
public void TryResolveSharpieCompanionFrameworkPath_UsesLaterExistingImport()
{
var tempRoot = Path.Combine(Path.GetTempPath(), $"firebase-binding-audit-companion-{Guid.NewGuid():N}");

try
{
var stagedFrameworksDirectory = Path.Combine(tempRoot, "frameworks");
var primaryFrameworkPath = Path.Combine(stagedFrameworksDirectory, "FirebaseCore.xcframework", "ios-arm64", "FirebaseCore.framework");
var primaryHeadersDirectory = Path.Combine(primaryFrameworkPath, "Headers");
var companionFrameworkPath = Path.Combine(stagedFrameworksDirectory, "FirebaseSessions.xcframework", "ios-arm64", "FirebaseSessions.framework");

Directory.CreateDirectory(primaryHeadersDirectory);
Directory.CreateDirectory(companionFrameworkPath);
File.WriteAllText(
Path.Combine(primaryHeadersDirectory, "FirebaseCore.h"),
"""
#import <Foundation/Foundation.h>
#import <FirebaseCore/FirebaseCore.h>
#import <FirebaseSessions/FirebaseSessions.h>
""");

var resolved = AuditRunner.TryResolveSharpieCompanionFrameworkPath(
primaryFrameworkPath,
stagedFrameworksDirectory,
out var resolvedCompanionFrameworkPath);

Assert.True(resolved);
Assert.Equal(companionFrameworkPath, resolvedCompanionFrameworkPath);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}

private static AuditOptions CreateAuditOptions(bool disableSharpie) =>
new(
RepoRoot: string.Empty,
OutputDirectory: string.Empty,
GeneratorVersion: "0.7.0",
SelectedTargets: null,
KeepTemp: false,
SharpiePath: null,
SharpieVersion: "26.3.0.11",
DisableSharpie: disableSharpie,
DisableSuppressions: false);

private static SharpieConfiguration CreateSharpieConfiguration() =>
new()
{
DefaultMode = "auto"
};

private static AuditTargetDefinition CreateTarget(string id, string xcframework, string? sharpieMode = null) =>
new()
{
Id = id,
Xcframework = xcframework,
SharpieMode = sharpieMode
};
}
Loading
Loading