Add native API versioning and compatibility guards - #126
Conversation
| @@ -0,0 +1,2 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.snap.valdi.empty" /> | |||
There was a problem hiding this comment.
Semgrep identified an issue in your code:
This literal might contain a Snapchat internal reference that should not be committed to open-source repositories.
Fix: Please replace / remove the string to avoid committing it to open-source repositories.
To resolve this comment:
✨ Commit fix suggestion
- Replace the internal package name in the manifest with a neutral open-source identifier that does not include
.snap, for example changepackage="com.snap.valdi.empty"to something likepackage="com.example.valdi.empty". - Keep the new package value consistent with the module’s intended public namespace so Android tooling still treats it as the app/package identifier.
- Alternatively, if this manifest is only a placeholder or test fixture, remove the
packageattribute entirely when it is not required by the file’s purpose.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by internal-sensitive-strings.
You can view more details about this finding in the Semgrep AppSec Platform.
Sensitive Files Detected🔧 Build rules — Affects build rules for all Valdi consumers. This is an automated notice. A maintainer will review after import. |
🎉 Bazel & CI Test Results
All Bazel configuration and CI tests passed! ✨ The build system and core tooling are working correctly. 🚀 Bazel remote cache is now enabled - future builds will be faster! Workflow: Valdi CI |
scottthompsonsc
left a comment
There was a problem hiding this comment.
Notes from reading through the versioning validator. Three findings, all about @Version propagation and guard narrowing. None look covered by VersioningValidator.spec.ts, so I may be misreading the intended semantics in places. Flagging as questions rather than blockers.
| for (const declaration of declarations) { | ||
| const version = this.getVersion(declaration); | ||
| if (version !== undefined) { | ||
| return version; | ||
| } | ||
| } | ||
|
|
||
| return undefined; |
There was a problem hiding this comment.
A member's @Version is read without its container's, so uses of it are not guarded
getVersionFromSymbol resolves the required version by calling getVersion(declaration) on the member declaration itself. For a member with no annotation of its own that returns undefined, even when the containing interface or class carries @Version(N), so validatePropertyAccess and getRequiredVersionForCall see no requirement and allow the access unguarded.
// @Version(5)
export interface Renderer {
draw(): void;
}
function render(r: Renderer) {
r.draw(); // no diagnostic, but this only exists at apiVersion >= 5
}This is the direction that fails unsafely: the call compiles and then hits a missing native implementation on an older runtime. validateContainerDeclaration already propagates the container version to heritage clauses and to property members, so the propagation exists for declaration sites but not for use sites.
Is container-level @Version meant to be inherited by members? If so this lookup needs to walk to the containing declaration. If not, it might be worth rejecting @Version on a container outright, so the annotation cannot read as covering its members.
Separately, the loop returns the first declaration that has a version rather than the maximum across declarations, which would matter for merged interface declarations.
| if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) { | ||
| return this.getVersion(node); | ||
| } |
There was a problem hiding this comment.
Class methods do not inherit their class's @Version during signature validation
getDeclarationVersion returns getVersion(node) for a method or accessor without consulting the parent, and validateContainerDeclaration skips function-like members, so a class method is only ever validated against its own annotation.
// @Version(5)
export class Foo {
bar(): SomeV5Type { ... } // "Type 'SomeV5Type' requires @Version(5) on the containing declaration"
}effectiveDeclarationVersion resolves to undefined here, so validateSignature reports a diagnostic for a signature the class's own version already covers, and every method has to repeat the annotation.
Interface methods are not affected, since isFunctionLikeDeclaration is false for MethodSignature and they keep going through validateContainerDeclaration to pick up memberVersion. That asymmetry between class and interface methods looks unintended.
| this.validateCallExpression(node, currentVersion); | ||
| } | ||
|
|
||
| ts.forEachChild(node, (child) => this.visit(child, currentVersion)); |
There was a problem hiding this comment.
Version guards only narrow inside if statements, so && and ternary guards fall through
visitVersionCondition handles && short-circuiting, but its only caller is visitIfStatement. Everywhere else visit reaches this forEachChild and descends into both sides of a &&, and both branches of a ternary, with the outer currentVersion, so the guard is never applied.
const canDraw = isVersionAtLeast(42) && model.v42Prop;
return isVersionAtLeast(42) && model.v42Prop;
{isVersionAtLeast(42) && <NewComponent />}
isVersionAtLeast(42) ? newApi() : oldApi();The JSX form is the one I would expect to hurt most, since {cond && <Component />} is the usual way to render conditionally. Every isVersionAtLeast case in VersioningValidator.spec.ts is an if condition, so this reads as untested rather than intentional.
Routing && binary expressions through visitVersionCondition from visit, plus a ts.isConditionalExpression case that visits whenTrue with the merged version, would cover these. !isVersionAtLeast(N) followed by an early return is a related gap.
| attrs = { | ||
| "deps": attr.label_list( | ||
| mandatory = True, | ||
| cfg = "exec", |
There was a problem hiding this comment.
Is cfg = "exec" intended on deps?
deps here only supplies ValdiModuleInfo.intermediates, which are compiler outputs rather than tools this rule runs, so the exec transition looks unnecessary. For a consumer that also depends on those modules in the target configuration it would build the module graph a second time in the exec configuration. Nothing instantiates valdi_compilation_metadata yet so this is inert today, just cheaper to settle now than after the first consumer lands.
There was a problem hiding this comment.
yes is it. cfg = exec is used for all valdi modules output. If you don't use it, modules might be built twice
Description
This change adds support for runtime versioning. It is designed to allow TS code to safely drift apart from its underlying native runtime.
The key concepts are as follow:
@Version()and an arbitrary int versionisVersionAtLeast().isVersionAtLeast()Type of Change
Testing
bazel test //...)Testing Details
Checklist
Related Issues
Additional Context