feat(codescan): C#/Unity support — scan the language most mobile games are written in - #29
feat(codescan): C#/Unity support — scan the language most mobile games are written in#29treenod-ollie wants to merge 2 commits into
Conversation
Unity is the dominant engine for mobile games, but greenlight's code scan
covered only Swift/ObjC/JS/TS — on a Unity project the flow rules judged the
app by its native plugin shims alone, producing both false positives and
blind spots.
- detectLanguage: recognize .cs as csharp (codescan + privacy scanner)
- rules: extend the generic rules to csharp; teach iap-no-restore Unity IAP
(UnityEngine.Purchasing, IStoreListener, IAppleExtensions.RestoreTransactions)
- claims: same patterns, so the runtime tier sees Unity flows too
- privacy: PlayerPrefs is NSUserDefaults-backed on Apple platforms — count it
as the User Defaults required-reason API; recognize Unity's ATT binding and
post-build NSUserTrackingUsageDescription injection
- skip Unity-generated dirs (Library/Temp/Logs/obj/UserSettings) — but only
when ProjectSettings/ProjectSettings.asset proves the root is a Unity project
Three false-positive fixes found while validating, each scoped so it cannot
trade a false positive for the much costlier false negative:
- account-no-delete: require a trailing word boundary so UIKit's
registerDefaults:@{@"UserAgent"...} no longer reads as account signup
(registerUser( still matches)
- uiwebview-removed: new deadCodeGuards suppress matches inside a
deployment-target branch (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0)
that no shipping build compiles. Suppression is line-scoped via real
#if/#else/#endif tracking — usage after the #endif, in the #else branch, or
in a file with unbalanced directives is still reported
- social-login-no-apple: recognize SIWA via provider constants
(APPLE_SIGNIN_RESULT_*), mirroring the google.*sign.*in trigger. Anti-pattern
matching now ignores comments project-wide, so `// TODO: Sign in with Apple
not supported` can no longer satisfy the rule it contradicts; string literals
still count, since SDK-driven implementations name providers in strings
Validated against a production Unity mobile game (~13k C# files): three false
positives eliminated, and the C# pass surfaced a real missing-privacy-manifest
issue (PlayerPrefs without NSPrivacyAccessedAPICategoryUserDefaults) that the
Swift-only scan could not see.
67a2fb6 to
8cc74d7
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
There are 4 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8cc74d7. Configure here.
Four correctness issues from Cursor Bugbot on RevylAI#29. Each was a false negative — a rule silently disabling itself — which is the costlier direction for a scanner whose job is to catch rejection reasons. - iap-no-restore: drop bare IAppleExtensions from the restore anti-pattern. That interface also carries deferred purchases, receipts and promo helpers, so reading a receipt looked like implementing restore. Only the actual RestoreTransactions call counts now - social-login-no-apple: 'apple.*sign.*in' also matched Apple *code signing* ('appleSigning', 'signing with apple'), which appears in essentially every iOS build script and would disable the 4.8 rule project-wide. Tightened to apple[_\s-]*sign[_\s-]?in([^gG]|$) so sign-IN matches and sign-ING does not - AntiPatternMatched: comment stripping was line-local, so continuation lines of a /* … */ block stayed exposed and a multi-line TODO could still satisfy an anti-pattern. Stripping now carries block state across the file - Unity generated dirs: matched by basename at every depth, so real game source under Assets/Scripts/Logs or a plugin's own Library/ was skipped. Now matched by full path, so only the editor's root-level output is skipped Tests cover each: receipt-only IAppleExtensions, code-signing text vs real SIWA provider constants, a two-line block comment, and a .cs file under Assets/Scripts/Logs surviving collection while root Library/ is skipped.
|
Thanks Bugbot — all four were real, and all four were false negatives (a rule quietly disabling itself), which is the costlier direction here. Fixed in c753348.
The "guard text suppresses whole file" finding was already addressed before this review landed: Each fix has a regression test. Re-validated against the same production Unity project: findings unchanged, no false positive resurrected. |
|
Ran this branch against a binary built from The rule changes are where I'd hold off. Four of them turn a finding 1. Dead-branch suppression inverts on compound conditions
#if !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0)
UIWebView *live = [[UIWebView alloc] initWithFrame:CGRectZero];
#endif
The doc comment says every ambiguity resolves toward live. An unevaluatable condition that merely contains the guard resolves toward dead, which switches off a hard rejection rule. Fix is to anchor the guard to the entire condition and treat anything with 2. // src/analytics.js
import * as Analytics from 'firebase-analytics';
Analytics.init({ adId: true });
// app.config.js
ios: { infoPlist: { NSUserTrackingUsageDescription: 'We use your data to personalize ads' } }
3. api.registerUserWithEmail(e, p) // main: HIGH 5.1.1 PR: nothing
api.registerUser(e, p) // both: HIGH 5.1.1
4. export const banner = "Sign in with Apple coming soon!";
export function googleSignIn() { return auth.signInWithGoogle(); }
5. Comment stripping leaks on escaped quotes void G() { GoogleSignin.SignIn(); }
string s = "quote: \" end"; // TODO: implement Sign in with Apple4.8 is suppressed project wide. The One more I did not reproduce, flagging it as a lead rather than a finding:
The direction here is right and the false positives you found are real. The thing I care about is that a false negative costs more than a false positive for this tool: a spurious finding wastes someone ten minutes, a missing one loses them a review cycle. Every fix above keeps your false positive fix intact. |

Why
Unity powers a large share of the mobile apps that go through App Review, but greenlight's code scan covers Swift/ObjC/JS/TS only. On a Unity project the flow rules judge the app by its native plugin shims alone, which cuts both ways: false positives (a webview shim's
registerDefaults:@{@"UserAgent"...}read as account signup) and blind spots (the actual IAP/login/deletion logic lives in C# the scanner never opens).We validated this branch against a production Unity mobile game (~13k C# files). Three false positives disappeared, and the C# pass surfaced a real issue the Swift-only scan could not see:
PlayerPrefsusage with noNSPrivacyAccessedAPICategoryUserDefaultsdeclaration.What
C# as a scanned language
.cs→csharpin both codescan and the privacy scannercsharpiap-no-restorelearns Unity IAP:UnityEngine.Purchasing,IStoreListener/IDetailedStoreListener/CodelessIAPStoreListeneras triggers;IAppleExtensions.RestoreTransactionsas the restore anti-patternDetectClaimsgets the same patterns, so the runtime tier sees Unity flows tooUnity-aware privacy scan
PlayerPrefsis NSUserDefaults-backed on Apple platforms → counts as the User Defaults required-reason APIFile.GetLastWriteTimeetc.) → File Timestamp categoryATTrackingStatusBinding) and post-buildNSUserTrackingUsageDescriptioninjection count as ATT implementationsUnity-generated directory skipping
Library/,Temp/,Logs/,obj/,UserSettings/are engine caches that dwarfAssets/by an order of magnitude — skipped, but only whenProjectSettings/ProjectSettings.assetproves the root is a Unity project, so a non-Unity repo with aLibrary/folder keeps full coverageFalse-positive fixes found during validation (not Unity-specific)
Each is scoped so it cannot trade a false positive for the much costlier false negative:
account-no-delete:register.*user→register.*user\b, so UIKit'sregisterDefaults:@{@"UserAgent"...}no longer reads as signup (registerUser(still matches)uiwebview-removed: newdeadCodeGuardssuppress matches inside a deployment-target branch (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) that no shipping build compiles — e.g. unity-webview's compatibility shim. Suppression is line-scoped through real#if/#else/#endiftracking (internal/codescan/preproc.go): usage after the#endif, in the#elsebranch, before the guard, or anywhere in a file with unbalanced directives is still reported. Every ambiguity resolves toward "live", so a hard-rejection rule is never silently disabledsocial-login-no-apple: auth SDKs abstract SIWA behind provider constants (APPLE_SIGNIN_RESULT_*) without ever namingASAuthorization*;apple.*sign.*innow counts as the anti-pattern, mirroring the existinggoogle.*sign.*intrigger. Anti-pattern matching now ignores comments project-wide, so// TODO: Sign in with Apple not supported yetcan no longer satisfy the rule it contradicts — string literals still count, since SDK-driven implementations name their providers in stringsTests
go test ./...passes. New coverage ininternal/codescan/csharp_unity_test.goandinternal/privacy/csharp_unity_test.gofor each behavior above, including the adversarial cases: live UIWebView after#endif/ in#else/ with unbalanced directives, nested dead branches, a TODO comment failing to satisfy the SIWA anti-pattern,//inside a URL string literal, the non-UnityLibrary/non-skip, and thePROVIDER_APPLE = "apple"non-match guard.🤖 Generated with Claude Code
Note
Medium Risk
Changes affect App Review–critical heuristics (UIWebView, SIWA, IAP restore, account deletion) and broaden what code is scanned; behavior is heavily tested but regex/preprocessor logic could still miss edge cases.
Overview
Adds C# / Unity as a first-class scan target:
.csfiles are walked (with root-levelLibrary/,Temp/, etc. skipped only whenProjectSettings/ProjectSettings.assetmarks a Unity project), flow rules andDetectClaimslearn Unity IAP / restore / SIWA patterns, and the privacy scanner treatsPlayerPrefs, .NET file timestamps, and Unity ATT/post-build plist injection like their native equivalents.Rule accuracy changes (scoped to avoid trading false positives for silent misses on critical rules):
account-no-deletetightens signup detection;uiwebview-removedgains line-scoped#ifdead-branch suppression viapreproc.go; global anti-pattern checks strip comments (including multiline blocks) while keeping string-literal SDK evidence; SIWA / restore / platform-reference patterns are tuned for Unity and auth SDK constants.Reviewed by Cursor Bugbot for commit c753348. Bugbot is set up for automated code reviews on this repo. Configure here.