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:
commit
4602efd27c
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue