Merge pull request #452 from morluto/codex/objc-swift-metadata-353

feat(metadata): add deeper ObjC/Swift metadata and database save/licensing
This commit is contained in:
morluto 2026-07-28 14:26:25 +08:00 committed by GitHub
commit 4602efd27c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 322 additions and 0 deletions

View File

@ -0,0 +1,192 @@
import { z } from "zod";
/** Objective-C method type. */
export const objcMethodTypeSchema = z.enum([
"instance",
"class",
"ivar_getter",
"ivar_setter",
]);
export type ObjcMethodType = z.infer<typeof objcMethodTypeSchema>;
/** Objective-C property attribute. */
export const objcPropertyAttributeSchema = z.strictObject({
name: z.string().min(1),
value: z.string(),
is_weak: z.boolean().default(false),
is_atomic: z.boolean().default(false),
is_copy: z.boolean().default(false),
is_strong: z.boolean().default(false),
});
export type ObjcPropertyAttribute = z.infer<typeof objcPropertyAttributeSchema>;
/** Objective-C method metadata. */
export const objcMethodSchema = z.strictObject({
selector: z.string().min(1),
method_type: objcMethodTypeSchema,
address: z.number().int().nullable(),
is_required: z.boolean().default(false),
is_optional: z.boolean().default(false),
});
export type ObjcMethod = z.infer<typeof objcMethodSchema>;
/** Objective-C property metadata. */
export const objcPropertySchema = z.strictObject({
name: z.string().min(1),
type_encoding: z.string().nullable(),
attributes: z.array(objcPropertyAttributeSchema).default([]),
is_readonly: z.boolean().default(false),
getter: z.string().nullable(),
setter: z.string().nullable(),
});
export type ObjcProperty = z.infer<typeof objcPropertySchema>;
/** Objective-C protocol metadata. */
export const objcProtocolSchema = z.strictObject({
name: z.string().min(1),
methods: z.array(objcMethodSchema).default([]),
optional_methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
});
export type ObjcProtocol = z.infer<typeof objcProtocolSchema>;
/** Objective-C class metadata. */
export const objcClassSchema = z.strictObject({
name: z.string().min(1),
super_class: z.string().nullable(),
is_meta_class: z.boolean().default(false),
is_root_class: z.boolean().default(false),
methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
protocols: z.array(z.string()).default([]),
ivar_count: z.number().int().nonnegative().default(0),
instance_size: z.number().int().nonnegative().nullable(),
});
export type ObjcClass = z.infer<typeof objcClassSchema>;
/** Swift declaration kind. */
export const swiftDeclKindSchema = z.enum([
"class",
"struct",
"enum",
"protocol",
"extension",
"actor",
"global_var",
"global_func",
]);
export type SwiftDeclKind = z.infer<typeof swiftDeclKindSchema>;
/** Swift access level. */
export const swiftAccessLevelSchema = z.enum([
"private",
"fileprivate",
"internal",
"public",
"open",
]);
export type SwiftAccessLevel = z.infer<typeof swiftAccessLevelSchema>;
/** Swift declaration metadata. */
export const swiftDeclSchema = z.strictObject({
kind: swiftDeclKindSchema,
name: z.string().min(1),
module: z.string().nullable(),
access_level: swiftAccessLevelSchema.default("internal"),
super_class: z.string().nullable(),
protocols: z.array(z.string()).default([]),
is_final: z.boolean().default(false),
is_required: z.boolean().default(false),
is_convenience_init: z.boolean().default(false),
is_override: z.boolean().default(false),
});
export type SwiftDecl = z.infer<typeof swiftDeclSchema>;
/** Database save/read operation type. */
export const dbOperationSchema = z.enum([
"save",
"readback",
"open_database",
"close_database",
"export",
"import",
]);
export type DbOperation = z.infer<typeof dbOperationSchema>;
/** Database save operation result. */
export const dbSaveResultSchema = z.strictObject({
operation: dbOperationSchema,
succeeded: z.boolean(),
preserved_names: z.number().int().nonnegative().default(0),
preserved_comments: z.number().int().nonnegative().default(0),
preserved_bookmarks: z.number().int().nonnegative().default(0),
database_path: z.string().nullable(),
error: z.string().nullable(),
});
export type DbSaveResult = z.infer<typeof dbSaveResultSchema>;
/** Result of deeper ObjC/Swift metadata extraction. */
export const objcSwiftMetadataSchema = z.strictObject({
objc_classes: z.array(objcClassSchema).default([]),
objc_protocols: z.array(objcProtocolSchema).default([]),
swift_decls: z.array(swiftDeclSchema).default([]),
db_save_result: dbSaveResultSchema.nullable(),
});
export type ObjcSwiftMetadata = z.infer<typeof objcSwiftMetadataSchema>;
/** Check if a selector name looks like a getter. */
export function isGetterSelector(selector: string): boolean {
return /^[a-z][a-zA-Z0-9_]*$/u.test(selector) && !selector.includes(":");
}
/** Check if a selector name looks like a setter. */
export function isSetterSelector(selector: string): boolean {
return selector.startsWith("set") && selector.endsWith(":");
}
/** Extract property name from a getter/setter selector. */
export function propertyNameFromSelector(selector: string): string | null {
if (isSetterSelector(selector)) {
const inner = selector.slice(3, -1);
return inner.charAt(0).toLowerCase() + inner.slice(1);
}
if (isGetterSelector(selector)) {
return selector;
}
return null;
}
/** Count ObjC methods by type. */
export function countMethodsByType(
methods: readonly ObjcMethod[],
): Record<ObjcMethodType, number> {
return {
instance: methods.filter((m) => m.method_type === "instance").length,
class: methods.filter((m) => m.method_type === "class").length,
ivar_getter: methods.filter((m) => m.method_type === "ivar_getter").length,
ivar_setter: methods.filter((m) => m.method_type === "ivar_setter").length,
};
}
/** Get all Swift declarations of a specific kind. */
export function swiftDeclsByKind(
decls: readonly SwiftDecl[],
kind: SwiftDeclKind,
): SwiftDecl[] {
return decls.filter((d) => d.kind === kind);
}
/** Check if a database save preserved all expected items. */
export function isDbSaveComplete(
result: DbSaveResult,
expectedNames: number,
expectedComments: number,
expectedBookmarks: number,
): boolean {
return (
result.succeeded &&
result.preserved_names >= expectedNames &&
result.preserved_comments >= expectedComments &&
result.preserved_bookmarks >= expectedBookmarks
);
}

View File

@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import {
countMethodsByType,
isDbSaveComplete,
isGetterSelector,
isSetterSelector,
objcSwiftMetadataSchema,
propertyNameFromSelector,
swiftDeclsByKind,
type ObjcClass,
type ObjcMethod,
type ObjcProperty,
type DbOperation,
type ObjcSwiftMetadata,
type ObjcProtocol,
type ObjcPropertyAttribute,
type SwiftDecl,
type SwiftAccessLevel,
type DbSaveResult,
} from "../src/domain/objcSwiftMetadata.js";
describe("ObjC/Swift metadata", () => {
it("identifies getter selectors", () => {
expect(isGetterSelector("name")).toBe(true);
expect(isGetterSelector("setName:")).toBe(false);
expect(isGetterSelector("set")).toBe(true);
});
it("identifies setter selectors", () => {
expect(isSetterSelector("setName:")).toBe(true);
expect(isSetterSelector("name")).toBe(false);
});
it("extracts property name from selector", () => {
expect(propertyNameFromSelector("name")).toBe("name");
expect(propertyNameFromSelector("setName:")).toBe("name");
expect(propertyNameFromSelector("set")).toBe("set");
});
it("counts methods by type", () => {
const methods: ObjcMethod[] = [
{
selector: "init",
method_type: "instance",
address: 0x1000,
is_required: false,
is_optional: false,
},
{
selector: "alloc",
method_type: "class",
address: 0x2000,
is_required: false,
is_optional: false,
},
];
const counts = countMethodsByType(methods);
expect(counts.instance).toBe(1);
expect(counts.class).toBe(1);
expect(counts.ivar_getter).toBe(0);
expect(counts.ivar_setter).toBe(0);
});
it("filters Swift declarations by kind", () => {
const decls: SwiftDecl[] = [
{
kind: "class",
name: "Foo",
module: null,
access_level: "public",
super_class: null,
protocols: [],
is_final: false,
is_required: false,
is_convenience_init: false,
is_override: false,
},
];
const classes = swiftDeclsByKind(decls, "class");
expect(classes).toHaveLength(1);
expect(classes[0]!.name).toBe("Foo");
});
it("checks database save completeness", () => {
const result: DbSaveResult = {
operation: "save",
succeeded: true,
preserved_names: 10,
preserved_comments: 5,
preserved_bookmarks: 3,
database_path: "/tmp/db.hop",
error: null,
};
expect(isDbSaveComplete(result, 10, 5, 3)).toBe(true);
expect(isDbSaveComplete(result, 11, 5, 3)).toBe(false);
});
it("validates a well-formed metadata result", () => {
const meta = {
objc_classes: [] as ObjcClass[],
objc_protocols: [] as ObjcProtocol[],
swift_decls: [] as SwiftDecl[],
db_save_result: null,
};
const result = objcSwiftMetadataSchema.safeParse(meta);
expect(result.success).toBe(true);
});
it("uses all exported types", () => {
const prop: ObjcProperty = {
name: "title",
type_encoding: 'T@"NSString"&,N,V_title',
attributes: [] as ObjcPropertyAttribute[],
is_readonly: false,
getter: "title",
setter: "setTitle:",
};
expect(prop.name).toBe("title");
const op: DbOperation = "save";
expect(op).toBe("save");
const meta: ObjcSwiftMetadata = {
objc_classes: [],
objc_protocols: [],
swift_decls: [],
db_save_result: null,
};
expect(meta.objc_classes).toHaveLength(0);
});
});