<src/argument-validator.ts>
L1: import {
L2: type Node,
L3: type Expression,
L4: type CallExpression,
L5: type ArrowFunctionExpression,
L6: type FunctionExpression,
L7: type ArrayExpression,
L8: type ObjectExpression,
L9: type StringLiteral,
L10: type RegExpLiteral,
L11: isCallExpression,
L12: isMemberExpression,
L13: type Identifier,
L14: } from "@babel/types";
L15: import { allowedZodMethods, allowedChainMethods } from "./zod-method-names";
L16: import type { ValidationConfig } from "./types";
L17: import { ResourceManager } from "./resource-manager";
L18: import { IssueReporter, IssueSeverity } from "./reporting";
L19: import { validateObjectExpression } from "./object-validator";
L20: import safeRegex from "safe-regex";
L21:
L22: /**
L23: * Validates arguments passed to Zod schema methods
L24: * Ensures arguments are safe and match expected patterns
L25: */
L26: export class ArgumentValidator {
L27: private readonly resourceManager: ResourceManager;
L28: private readonly issueReporter: IssueReporter;
L29:
L30: constructor(
L31: private readonly config: ValidationConfig,
L32: resourceManager?: ResourceManager,
L33: issueReporter?: IssueReporter,
L34: ) {
L35: this.resourceManager = resourceManager ?? new ResourceManager(config);
L36: this.issueReporter = issueReporter ?? new IssueReporter();
L37: }
L38:
L39: /**
L40: * Method-specific argument validation rules
L41: */
L42: private static readonly METHOD_RULES: Record<string, ArgumentRule> = {
L43: refine: {
L44: minArgs: 1,
L45: maxArgs: 2,
L46: allowFunction: true,
L47: allowSchema: false,
L48: validateFunction: true,
L49: },
L50: transform: {
L51: minArgs: 1,
L52: maxArgs: 1,
L53: allowFunction: true,
L54: allowSchema: false,
L55: validateFunction: true,
L56: },
L57: pipe: {
L58: minArgs: 1,
L59: maxArgs: 1,
L60: allowFunction: false,
L61: allowSchema: true,
L62: validateFunction: false,
L63: },
L64: regex: {
L65: minArgs: 1,
L66: maxArgs: 2,
L67: allowFunction: false,
L68: allowSchema: false,
L69: validateRegex: true,
L70: },
L71: object: {
L72: minArgs: 1,
L73: maxArgs: 1,
L74: allowFunction: false,
L75: allowSchema: false,
L76: },
L77: // Add more method rules as needed
L78: };
L79:
L80: /**
L81: * Validates arguments for a specific method call
L82: */
L83: public validateMethodArguments(
L84: node: CallExpression,
L85: methodName: string,
L86: ): boolean {
L87: const rules = ArgumentValidator.METHOD_RULES[methodName];
L88: if (!rules) {
L89: return true; // No specific rules for this method
L90: }
L91:
L92: try {
L93: // Check argument count
L94: if (!this.validateArgumentCount(node, rules)) {
L95: return false;
L96: }
L97:
L98: // Validate each argument
L99: return node.arguments.every((arg, index) =>
L100: this.validateArgument(arg, rules, methodName, index),
L101: );
L102: } catch (error) {
L103: if (error instanceof Error) {
L104: this.issueReporter.reportIssue(
L105: node,
L106: error.message,
L107: node.type,
L108: IssueSeverity.ERROR,
L109: );
L110: }
L111: return false;
L112: }
L113: }
L114:
L115: /**
L116: * Validates a single argument against rules
L117: */
L118: private validateArgument(
L119: arg: Node,
L120: rules: ArgumentRule,
L121: methodName: string,
L122: index: number,
L123: ): boolean {
L124: this.resourceManager.incrementNodeCount();
L125:
L126: // If the method requires the first argument to be a function
L127: // (e.g., refine or transform), and it's not a function,
L128: // return false immediately.
L129: if (methodName === "refine" && index === 0 && !isFunction(arg)) {
L130: this.issueReporter.reportIssue(
L131: arg,
L132: `The first argument to ${methodName} must be a function`,
L133: arg.type,
L134: IssueSeverity.ERROR,
L135: );
L136: return false;
L137: }
L138:
L139: // Handle different argument types
L140: if (isFunction(arg)) {
L141: return this.validateFunctionArgument(arg, rules);
L142: }
L143:
L144: if (isObjectExpression(arg)) {
L145: // If this method does not allow objects (unless explicitly stated),
L146: // and we got an object when a function was expected, fail.
L147: // For `refine`, only the second argument (options) might be an object.
L148: // If index is 0 and we are here, we already handled above. If index > 0, you may allow.
L149: if (methodName === "refine" && index === 0) {
L150: return false;
L151: }
L152: return validateObjectExpression(arg, 0, this.config, []).isValid;
L153: }
L154:
L155: if (isArrayExpression(arg)) {
L156: return this.validateArrayArgument(arg);
L157: }
L158:
L159: if (isLiteral(arg)) {
L160: return this.validateLiteralArgument(arg as Expression);
L161: }
L162:
L163: if (isIdentifier(arg)) {
L164: return this.validateIdentifierArgument(arg);
L165: }
L166:
L167: if (isCallExpression(arg)) {
L168: const callee = arg.callee;
L169: if (
L170: isMemberExpression(callee) &&
L171: isIdentifier(callee.object) &&
L172: callee.object.name === "z"
L173: ) {
L174: if (isIdentifier(callee.property)) {
L175: const methodName = callee.property.name;
L176: if (
L177: allowedZodMethods.has(methodName) ||
L178: allowedChainMethods.has(methodName)
L179: ) {
L180: return true;
L181: }
L182: } else {
L183: return false;
L184: }
L185: } else if (isIdentifier(callee) && callee.name === "z") {
L186: return true;
L187: }
L188: }
L189:
L190: // Unknown argument type
L191: this.issueReporter.reportIssue(
L192: arg,
L193: `Unexpected argument type for method ${methodName}: ${arg.type}`,
L194: arg.type,
L195: IssueSeverity.ERROR,
L196: );
L197: return false;
L198: }
L199:
L200: /**
L201: * Validates function arguments (for refine/transform)
L202: */
L203: private validateFunctionArgument(
L204: node: ArrowFunctionExpression | FunctionExpression,
L205: rules: ArgumentRule,
L206: ): boolean {
L207: if (!rules.allowFunction) {
L208: this.issueReporter.reportIssue(
L209: node,
L210: "Function arguments not allowed for this method",
L211: node.type,
L212: IssueSeverity.ERROR,
L213: );
L214: return false;
L215: }
L216:
L217: if (rules.validateFunction) {
L218: return this.validateFunctionBody(node);
L219: }
L220:
L221: return true;
L222: }
L223:
L224: /**
L225: * Validates function bodies for safety
L226: */
L227: private validateFunctionBody(
L228: node: ArrowFunctionExpression | FunctionExpression,
L229: ): boolean {
L230: // Don't allow async functions
L231: if (node.async) {
L232: this.issueReporter.reportIssue(
L233: node,
L234: "Async functions not allowed in schema validation",
L235: node.type,
L236: IssueSeverity.ERROR,
L237: );
L238: return false;
L239: }
L240:
L241: // Don't allow generators
L242: if (node.generator) {
L243: this.issueReporter.reportIssue(
L244: node,
L245: "Generator functions not allowed in schema validation",
L246: node.type,
L247: IssueSeverity.ERROR,
L248: );
L249: return false;
L250: }
L251:
L252: // Validate function body
L253: return this.validateFunctionStatements(node.body);
L254: }
L255:
L256: /**
L257: * Validates statements within a function body
L258: */
L259: private validateFunctionStatements(node: Node): boolean {
L260: // TODO: Implement based on your security requirements
L261: return true; // Placeholder
L262: }
L263:
L264: /**
L265: * Validates array arguments
L266: */
L267: private validateArrayArgument(node: ArrayExpression): boolean {
L268: // Check array size
L269: if (node.elements.length > this.config.maxPropertiesPerObject) {
L270: this.issueReporter.reportIssue(
L271: node,
L272: `Array exceeds maximum size of ${this.config.maxPropertiesPerObject}`,
L273: node.type,
L274: IssueSeverity.ERROR,
L275: );
L276: return false;
L277: }
L278:
L279: // Validate each element
L280: return node.elements.every((element) => {
L281: if (!element) return true; // Skip sparse array elements
L282: return this.validateArgument(
L283: element,
L284: { allowFunction: false, allowSchema: false },
L285: "array",
L286: 0,
L287: );
L288: });
L289: }
L290:
L291: /**
L292: * Validates literal arguments (string, number, boolean, etc.)
L293: */
L294: private validateLiteralArgument(node: Expression): boolean {
L295: if (isStringLiteral(node)) {
L296: return node.value.length <= this.config.maxStringLength;
L297: }
L298:
L299: if (isRegExpLiteral(node)) {
L300: return this.validateRegexLiteral(node);
L301: }
L302:
L303: // Other literals are generally safe
L304: return true;
L305: }
L306:
L307: /**
L308: * Validates regex literals for safety
L309: */
L310: private validateRegexLiteral(node: RegExpLiteral): boolean {
L311: try {
L312: // Check regex pattern length
L313: if (node.pattern.length > this.config.maxStringLength) {
L314: this.issueReporter.reportIssue(
L315: node,
L316: "Regex pattern too long",
L317: node.type,
L318: IssueSeverity.ERROR,
L319: );
L320: return false;
L321: }
L322:
L323: if (!safeRegex(node.pattern)) {
L324: this.issueReporter.reportIssue(
L325: node,
L326: "Regex pattern is not safe (as reported by safe-regex)",
L327: node.type,
L328: IssueSeverity.ERROR,
L329: );
L330: return false;
L331: }
L332:
L333: // Could add additional regex safety checks here
L334: return true;
L335: } catch {
L336: return false;
L337: }
L338: }
L339:
L340: /**
L341: * Validates identifier arguments
L342: */
L343: private validateIdentifierArgument(node: Identifier): boolean {
L344: // Could add checks for specific identifiers here
L345: return true;
L346: }
L347:
L348: /**
L349: * Validates argument count against rules
L350: */
L351: private validateArgumentCount(
L352: node: CallExpression,
L353: rules: ArgumentRule,
L354: ): boolean {
L355: const { minArgs, maxArgs } = rules;
L356: const argCount = node.arguments.length;
L357:
L358: if (minArgs !== undefined && argCount < minArgs) {
L359: this.issueReporter.reportIssue(
L360: node,
L361: `Too few arguments. Expected at least ${minArgs}, got ${argCount}`,
L362: node.type,
L363: IssueSeverity.ERROR,
L364: );
L365: return false;
L366: }
L367:
L368: if (maxArgs !== undefined && argCount > maxArgs) {
L369: this.issueReporter.reportIssue(
L370: node,
L371: `Too many arguments. Expected at most ${maxArgs}, got ${argCount}`,
L372: node.type,
L373: IssueSeverity.ERROR,
L374: );
L375: return false;
L376: }
L377:
L378: return true;
L379: }
L380: }
L381:
L382: /**
L383: * Rules for method argument validation
L384: */
L385: interface ArgumentRule {
L386: minArgs?: number;
L387: maxArgs?: number;
L388: allowFunction?: boolean;
L389: allowSchema?: boolean;
L390: validateFunction?: boolean;
L391: validateRegex?: boolean;
L392: }
L393:
L394: // Type guards
L395: function isFunction(
L396: node: Node,
L397: ): node is ArrowFunctionExpression | FunctionExpression {
L398: return (
L399: node.type === "ArrowFunctionExpression" ||
L400: node.type === "FunctionExpression"
L401: );
L402: }
L403:
L404: function isObjectExpression(node: Node): node is ObjectExpression {
L405: return node.type === "ObjectExpression";
L406: }
L407:
L408: function isArrayExpression(node: Node): node is ArrayExpression {
L409: return node.type === "ArrayExpression";
L410: }
L411:
L412: function isStringLiteral(node: Node): node is StringLiteral {
L413: return node.type === "StringLiteral";
L414: }
L415:
L416: function isRegExpLiteral(node: Node): node is RegExpLiteral {
L417: return node.type === "RegExpLiteral";
L418: }
L419:
L420: function isLiteral(node: Node): boolean {
L421: return (
L422: node.type === "StringLiteral" ||
L423: node.type === "NumericLiteral" ||
L424: node.type === "BooleanLiteral" ||
L425: node.type === "RegExpLiteral" ||
L426: node.type === "NullLiteral"
L427: );
L428: }
L429:
L430: function isIdentifier(node: Node): node is Identifier {
L431: return node.type === "Identifier";
L432: }
L433:
</src/argument-validator.ts>
<src/chain-validator.ts>
L1: import type {
L2: Node,
L3: CallExpression,
L4: MemberExpression,
L5: Identifier,
L6: Expression,
L7: } from "@babel/types";
L8: import type { ValidationConfig } from "./types";
L9: import { ResourceManager } from "./resource-manager";
L10: import { IssueReporter, IssueSeverity } from "./reporting";
L11: import { allowedChainMethods, allowedZodMethods } from "./zod-method-names";
L12: import { ArgumentValidator } from "./argument-validator";
L13:
L14: /**
L15: * Validates method chains in Zod schemas
L16: * Ensures proper chaining depth and only allowed methods are used
L17: */
L18: export class ChainValidator {
L19: private readonly resourceManager: ResourceManager;
L20: private readonly issueReporter: IssueReporter;
L21: private readonly argumentValidator: ArgumentValidator;
L22:
L23: constructor(
L24: private readonly config: ValidationConfig,
L25: resourceManager?: ResourceManager,
L26: issueReporter?: IssueReporter,
L27: argumentValidator?: ArgumentValidator,
L28: ) {
L29: this.resourceManager = resourceManager ?? new ResourceManager(config);
L30: this.issueReporter = issueReporter ?? new IssueReporter();
L31: this.argumentValidator =
L32: argumentValidator ??
L33: new ArgumentValidator(config, this.resourceManager, this.issueReporter);
L34: }
L35:
L36: /**
L37: * Validates a chain of method calls starting from a node
L38: * @param node - The starting node of the chain
L39: * @returns boolean indicating if the chain is valid
L40: */
L41: public validateChain(node: Node): boolean {
L42: try {
L43: return this.validateChainNode(node, 0);
L44: } catch (error) {
L45: if (error instanceof Error) {
L46: this.issueReporter.reportIssue(
L47: node,
L48: error.message,
L49: node.type,
L50: IssueSeverity.ERROR,
L51: );
L52: }
L53: return false;
L54: }
L55: }
L56:
L57: /**
L58: * Recursively validates a node in the method chain
L59: * @param node - Current node to validate
L60: * @param depth - Current depth in the chain
L61: * @returns boolean indicating if the node and its chain are valid
L62: */
L63: private validateChainNode(node: Node, depth: number): boolean {
L64: this.resourceManager.incrementNodeCount();
L65:
L66: // Enforce chain depth here if you want to fail gracefully instead of throwing
L67: if (depth > this.config.maxChainDepth) {
L68: this.issueReporter.reportIssue(
L69: node,
L70: `Chain nesting depth exceeded maximum of ${this.config.maxChainDepth}`,
L71: node.type,
L72: IssueSeverity.ERROR,
L73: );
L74: return false;
L75: }
L76:
L77: this.resourceManager.trackDepth(depth, "chain");
L78:
L79: if (isIdentifier(node)) {
L80: return this.validateIdentifier(node);
L81: }
L82:
L83: if (isMemberExpression(node)) {
L84: return this.validateMemberExpression(node, depth);
L85: }
L86:
L87: if (isCallExpression(node)) {
L88: return this.validateCallExpression(node, depth);
L89: }
L90:
L91: this.issueReporter.reportIssue(
L92: node,
L93: `Unexpected node type in chain: ${node.type}`,
L94: node.type,
L95: IssueSeverity.ERROR,
L96: );
L97: return false;
L98: }
L99:
L100: /**
L101: * Validates a call expression node (e.g., z.string(), .optional())
L102: */
L103: private validateCallExpression(node: CallExpression, depth: number): boolean {
L104: // Validate the callee first
L105: const callee = node.callee as Expression;
L106:
L107: if (!this.validateChainNode(callee, depth + 1)) {
L108: return false;
L109: }
L110:
L111: // Get the method name being called
L112: const methodName = this.getMethodName(callee);
L113: if (!methodName) {
L114: this.issueReporter.reportIssue(
L115: node,
L116: "Unable to determine method name",
L117: node.type,
L118: IssueSeverity.ERROR,
L119: );
L120: return false;
L121: }
L122:
L123: // Validate method arguments if needed
L124: if (this.requiresArgumentValidation(methodName)) {
L125: return this.validateMethodArguments(node, methodName);
L126: }
L127:
L128: return true;
L129: }
L130:
L131: /**
L132: * Validates a member expression (e.g., z.string, .optional)
L133: */
L134: private validateMemberExpression(
L135: node: MemberExpression,
L136: depth: number,
L137: ): boolean {
L138: if (node.computed) {
L139: this.issueReporter.reportIssue(
L140: node,
L141: "Computed properties not allowed in chain",
L142: node.type,
L143: IssueSeverity.ERROR,
L144: );
L145: return false;
L146: }
L147:
L148: // Validate the object part of the member expression
L149: if (!this.validateChainNode(node.object, depth + 1)) {
L150: return false;
L151: }
L152:
L153: // Validate the property name
L154: if (!isIdentifier(node.property)) {
L155: this.issueReporter.reportIssue(
L156: node.property,
L157: "Property must be an identifier",
L158: node.property.type,
L159: IssueSeverity.ERROR,
L160: );
L161: return false;
L162: }
L163:
L164: const methodName = node.property.name;
L165: if (!this.isMethodAllowed(methodName)) {
L166: this.issueReporter.reportIssue(
L167: node,
L168: `Method not allowed in chain: ${methodName}`,
L169: node.type,
L170: IssueSeverity.ERROR,
L171: "Use only allowed Zod methods",
L172: );
L173: return false;
L174: }
L175:
L176: return true;
L177: }
L178:
L179: /**
L180: * Validates an identifier node (should be 'z')
L181: */
L182: private validateIdentifier(node: Identifier): boolean {
L183: if (node.name !== "z") {
L184: this.issueReporter.reportIssue(
L185: node,
L186: `Chain must start with 'z', found: ${node.name}`,
L187: node.type,
L188: IssueSeverity.ERROR,
L189: );
L190: return false;
L191: }
L192: return true;
L193: }
L194:
L195: /**
L196: * Gets the method name from a node
L197: */
L198: private getMethodName(node: Expression): string | null {
L199: if (isIdentifier(node)) {
L200: return node.name;
L201: }
L202: if (isMemberExpression(node) && isIdentifier(node.property)) {
L203: return node.property.name;
L204: }
L205: return null;
L206: }
L207:
L208: /**
L209: * Checks if a method name is allowed
L210: */
L211: private isMethodAllowed(methodName: string): boolean {
L212: return (
L213: allowedZodMethods.has(methodName) || allowedChainMethods.has(methodName)
L214: );
L215: }
L216:
L217: /**
L218: * Checks if a method requires argument validation
L219: */
L220: private requiresArgumentValidation(methodName: string): boolean {
L221: // Add methods that need argument validation
L222: return ["refine", "transform", "pipe", "object"].includes(methodName);
L223: }
L224:
L225: /**
L226: * Validates arguments for specific methods
L227: */
L228: private validateMethodArguments(
L229: node: CallExpression,
L230: methodName: string,
L231: ): boolean {
L232: return this.argumentValidator.validateMethodArguments(node, methodName);
L233: }
L234: }
L235:
L236: // Type guards
L237: function isIdentifier(node: Node): node is Identifier {
L238: return node.type === "Identifier";
L239: }
L240:
L241: function isMemberExpression(node: Node): node is MemberExpression {
L242: return node.type === "MemberExpression";
L243: }
L244:
L245: function isCallExpression(node: Node): node is CallExpression {
L246: return node.type === "CallExpression";
L247: }
L248:
</src/chain-validator.ts>
<src/index.ts>
L1: import type { Issue } from "./reporting";
L2: import { validateSchema } from "./schema-validator";
L3: import { createConfig, relaxedConfig, type ValidationConfig } from "./types";
L4:
L5: // Core validators and types
L6: export { SchemaValidator, validateSchema } from "./schema-validator";
L7: export type { ValidationConfig, PropertySafetyConfig } from "./types";
L8: export { IssueReporter, IssueSeverity, type Issue } from "./reporting";
L9: export {
L10: ResourceManager,
L11: ValidationError,
L12: type ResourceStats,
L13: } from "./resource-manager";
L14:
L15: // Preset configurations
L16: export {
L17: extremelySafeConfig,
L18: mediumConfig,
L19: relaxedConfig,
L20: createConfig,
L21: } from "./types";
L22:
L23: // Main validation function with default config
L24: export async function validateZodSchema(
L25: schemaCode: string,
L26: config: Partial<ValidationConfig> = {},
L27: ): Promise<{
L28: isValid: boolean;
L29: cleanedCode: string;
L30: issues: Array<Issue>;
L31: }> {
L32: const finalConfig = createConfig(relaxedConfig, config);
L33: return validateSchema(schemaCode, finalConfig);
L34: }
L35:
L36: // Example usage in comments
L37: /*
L38: import { validateZodSchema, extremelySafeConfig } from 'zod-validator';
L39:
L40: // Using default config (medium)
L41: const result = await validateZodSchema(`
L42: import { z } from 'zod';
L43: export const userSchema = z.object({
L44: name: z.string(),
L45: age: z.number()
L46: });
L47: `);
L48:
L49: // Using specific config
L50: const resultWithConfig = await validateZodSchema(schemaCode, {
L51: timeoutMs: 2000,
L52: maxNodeCount: 5000
L53: });
L54:
L55: // Using preset config
L56: const safeResult = await validateZodSchema(schemaCode, extremelySafeConfig);
L57: */
L58:
</src/index.ts>
<src/object-validator.ts>
L1: import type {
L2: Node,
L3: ObjectExpression,
L4: ObjectProperty,
L5: ObjectMethod,
L6: Identifier,
L7: StringLiteral,
L8: } from "@babel/types";
L9: import type { ValidationConfig } from "./types";
L10: import { type Issue, IssueSeverity } from "./reporting";
L11:
L12: /**
L13: * Result of validating a node, including any issues found
L14: */
L15: interface ValidationResult {
L16: isValid: boolean;
L17: issues: Issue[];
L18: }
L19:
L20: /**
L21: * Cache for validation results to avoid re-processing nodes
L22: * Uses WeakMap to allow garbage collection of processed nodes
L23: */
L24: const validationCache = new WeakMap<Node, ValidationResult>();
L25:
L26: /**
L27: * Validates an object expression against configured safety rules
L28: * Checks property count, depth, and property name safety
L29: *
L30: * @param obj - The object expression node to validate
L31: * @param depth - Current depth in the object hierarchy
L32: * @param config - Validation configuration settings
L33: * @param parentNodes - Stack of parent nodes for context
L34: * @returns ValidationResult indicating if the object is valid
L35: */
L36: export function validateObjectExpression(
L37: obj: ObjectExpression,
L38: depth: number,
L39: config: ValidationConfig,
L40: parentNodes: Node[] = [],
L41: ): ValidationResult {
L42: // Check cache first
L43: const cached = validationCache.get(obj);
L44: if (config.enableCaching && cached) {
L45: return cached;
L46: }
L47:
L48: const issues: Issue[] = [];
L49:
L50: // Check depth
L51: if (depth >= config.maxObjectDepth) {
L52: issues.push({
L53: line: obj.loc?.start.line ?? -1,
L54: column: obj.loc?.start.column,
L55: message: `Object exceeds maximum nesting depth of ${config.maxObjectDepth}`,
L56: severity: IssueSeverity.ERROR,
L57: nodeType: "ObjectExpression",
L58: });
L59: return cacheAndReturn(obj, config, { isValid: false, issues });
L60: }
L61:
L62: // Check property count
L63: if (obj.properties.length > config.maxPropertiesPerObject) {
L64: issues.push({
L65: line: obj.loc?.start.line ?? -1,
L66: column: obj.loc?.start.column,
L67: message: `Object exceeds maximum property count of ${config.maxPropertiesPerObject}`,
L68: severity: IssueSeverity.ERROR,
L69: nodeType: "ObjectExpression",
L70: });
L71: return cacheAndReturn(obj, config, { isValid: false, issues });
L72: }
L73:
L74: // Validate each property
L75: for (const prop of obj.properties) {
L76: if (prop.type === "ObjectProperty") {
L77: const propResult = validateProperty(prop, config, [...parentNodes, obj]);
L78: if (!propResult.isValid) {
L79: issues.push(...propResult.issues);
L80: return cacheAndReturn(obj, config, { isValid: false, issues });
L81: }
L82: // Check if the property value is also an object
L83: if (prop.value && prop.value.type === "ObjectExpression") {
L84: const nestedResult = validateObjectExpression(
L85: prop.value,
L86: depth + 1,
L87: config,
L88: [...parentNodes, obj],
L89: );
L90: if (!nestedResult.isValid) {
L91: issues.push(...nestedResult.issues);
L92: return cacheAndReturn(obj, config, { isValid: false, issues });
L93: }
L94: }
L95: } else if (prop.type === "ObjectMethod") {
L96: const propResult = validateProperty(prop, config, [...parentNodes, obj]);
L97: if (!propResult.isValid) {
L98: issues.push(...propResult.issues);
L99: return cacheAndReturn(obj, config, { isValid: false, issues });
L100: }
L101: } else if (prop.type === "SpreadElement") {
L102: // Already handled above
L103: issues.push({
L104: line: prop.loc?.start.line ?? -1,
L105: column: prop.loc?.start.column,
L106: message: "Spread elements are not allowed in objects",
L107: severity: IssueSeverity.ERROR,
L108: nodeType: "SpreadElement",
L109: });
L110: return cacheAndReturn(obj, config, { isValid: false, issues });
L111: }
L112: }
L113:
L114: return cacheAndReturn(obj, config, { isValid: true, issues });
L115: }
L116:
L117: /**
L118: * Validates a single object property or method
L119: * Checks for unsafe property names, getters/setters, and computed properties
L120: *
L121: * @param prop - The property node to validate
L122: * @param config - Validation configuration settings
L123: * @param parentNodes - Stack of parent nodes for context
L124: * @returns ValidationResult indicating if the property is valid
L125: */
L126: function validateProperty(
L127: prop: ObjectProperty | ObjectMethod,
L128: config: ValidationConfig,
L129: parentNodes: Node[],
L130: ): ValidationResult {
L131: const issues: Issue[] = [];
L132:
L133: // Check for computed properties
L134: if (prop.computed && !config.allowComputedProperties) {
L135: issues.push({
L136: line: prop.loc?.start.line ?? -1,
L137: column: prop.loc?.start.column,
L138: message: "Computed properties are not allowed",
L139: severity: IssueSeverity.ERROR,
L140: nodeType: prop.type,
L141: });
L142: return { isValid: false, issues };
L143: }
L144:
L145: // Check for getters/setters
L146: if (isObjectMethod(prop) && (prop.kind === "get" || prop.kind === "set")) {
L147: issues.push({
L148: line: prop.loc?.start.line ?? -1,
L149: column: prop.loc?.start.column,
L150: message: "Getter/setter methods are not allowed",
L151: severity: IssueSeverity.ERROR,
L152: nodeType: "ObjectMethod",
L153: });
L154: return { isValid: false, issues };
L155: }
L156:
L157: // Validate property name
L158: const nameResult = validatePropertyName(prop.key, config);
L159: if (!nameResult.isValid) {
L160: issues.push(...nameResult.issues);
L161: return { isValid: false, issues };
L162: }
L163:
L164: return { isValid: true, issues };
L165: }
L166:
L167: /**
L168: * Validates a property name against safety rules
L169: * Checks against allowed/denied lists and prefixes
L170: *
L171: * @param key - The property key node to validate
L172: * @param config - Validation configuration settings
L173: * @returns ValidationResult indicating if the property name is safe
L174: */
L175: function validatePropertyName(
L176: key: Node,
L177: config: ValidationConfig,
L178: ): ValidationResult {
L179: // Only handle identifier and string literal keys
L180: if (!isIdentifier(key) && !isStringLiteral(key)) {
L181: return {
L182: isValid: false,
L183: issues: [
L184: {
L185: line: key.loc?.start.line ?? -1,
L186: column: key.loc?.start.column,
L187: message: "Property key must be an identifier or string literal",
L188: severity: IssueSeverity.ERROR,
L189: nodeType: key.type,
L190: },
L191: ],
L192: };
L193: }
L194:
L195: const name = isIdentifier(key) ? key.name : key.value;
L196: const { propertySafety } = config;
L197:
L198: if (name === "__proto__") {
L199: return {
L200: isValid: false,
L201: issues: [
L202: {
L203: line: key.loc?.start.line ?? -1,
L204: column: key.loc?.start.column,
L205: message: `Property name '${name}' is not allowed`,
L206: severity: IssueSeverity.ERROR,
L207: nodeType: key.type,
L208: },
L209: ],
L210: };
L211: }
L212:
L213: // Check against denied properties
L214: if (propertySafety.deniedProperties.has(name)) {
L215: return {
L216: isValid: false,
L217: issues: [
L218: {
L219: line: key.loc?.start.line ?? -1,
L220: column: key.loc?.start.column,
L221: message: `Property name '${name}' is not allowed`,
L222: severity: IssueSeverity.WARNING,
L223: nodeType: key.type,
L224: },
L225: ],
L226: };
L227: }
L228:
L229: // Check against denied prefixes
L230: if (propertySafety.deniedPrefixes.some((prefix) => name.startsWith(prefix))) {
L231: return {
L232: isValid: false,
L233: issues: [
L234: {
L235: line: key.loc?.start.line ?? -1,
L236: column: key.loc?.start.column,
L237: message: `Property name '${name}' uses a forbidden prefix`,
L238: severity: IssueSeverity.ERROR,
L239: nodeType: key.type,
L240: },
L241: ],
L242: };
L243: }
L244:
L245: // If we're using a whitelist, check against allowed properties
L246: if (
L247: propertySafety.allowedProperties.size > 0 &&
L248: !propertySafety.allowedProperties.has(name)
L249: ) {
L250: return {
L251: isValid: false,
L252: issues: [
L253: {
L254: line: key.loc?.start.line ?? -1,
L255: column: key.loc?.start.column,
L256: message: `Property name '${name}' is not in the allowed list`,
L257: severity: IssueSeverity.ERROR,
L258: nodeType: key.type,
L259: },
L260: ],
L261: };
L262: }
L263:
L264: return { isValid: true, issues: [] };
L265: }
L266:
L267: /**
L268: * Type guards for node types
L269: */
L270: function isObjectProperty(node: Node): node is ObjectProperty {
L271: return node.type === "ObjectProperty";
L272: }
L273:
L274: function isObjectMethod(node: Node): node is ObjectMethod {
L275: return node.type === "ObjectMethod";
L276: }
L277:
L278: function isIdentifier(node: Node): node is Identifier {
L279: return node.type === "Identifier";
L280: }
L281:
L282: function isStringLiteral(node: Node): node is StringLiteral {
L283: return node.type === "StringLiteral";
L284: }
L285:
L286: /**
L287: * Helper to cache and return validation results
L288: */
L289: function cacheAndReturn(
L290: obj: ObjectExpression, // Add obj parameter
L291: config: ValidationConfig, // Add config parameter
L292: result: ValidationResult,
L293: ): ValidationResult {
L294: if (config.enableCaching) {
L295: validationCache.set(obj, result);
L296: }
L297: return result;
L298: }
L299:
</src/object-validator.ts>
<src/reporting.ts>
L1: import type { Node } from "@babel/types";
L2:
L3: /**
L4: * Represents a validation issue found during processing
L5: */
L6: export interface Issue {
L7: line: number;
L8: column?: number;
L9: message: string;
L10: nodeType: string;
L11: suggestion?: string;
L12: severity: IssueSeverity;
L13: source?: string;
L14: }
L15:
L16: /**
L17: * Severity levels for validation issues
L18: */
L19: export enum IssueSeverity {
L20: ERROR = "error",
L21: WARNING = "warning",
L22: INFO = "info",
L23: }
L24:
L25: /**
L26: * Class to manage validation issues and reporting
L27: */
L28: export class IssueReporter {
L29: private issues: Issue[] = [];
L30:
L31: /**
L32: * Reports a new validation issue
L33: */
L34: public reportIssue(
L35: node: Node,
L36: message: string,
L37: nodeType: string,
L38: severity: IssueSeverity = IssueSeverity.ERROR,
L39: suggestion?: string,
L40: ): void {
L41: this.issues.push({
L42: line: node.loc?.start.line ?? -1,
L43: column: node.loc?.start.column,
L44: message,
L45: nodeType,
L46: suggestion,
L47: severity,
L48: source: this.getSourceSnippet(node),
L49: });
L50: }
L51:
L52: /**
L53: * Gets all reported issues
L54: */
L55: public getIssues(): Issue[] {
L56: return [...this.issues];
L57: }
L58:
L59: /**
L60: * Gets issues filtered by severity
L61: */
L62: public getIssuesBySeverity(severity: IssueSeverity): Issue[] {
L63: return this.issues.filter((issue) => issue.severity === severity);
L64: }
L65:
L66: /**
L67: * Checks if there are any issues of ERROR severity
L68: */
L69: public hasErrors(): boolean {
L70: return this.issues.some((issue) => issue.severity === IssueSeverity.ERROR);
L71: }
L72:
L73: /**
L74: * Clears all reported issues
L75: */
L76: public clear(): void {
L77: this.issues = [];
L78: }
L79:
L80: /**
L81: * Gets a formatted report of all issues
L82: */
L83: public getFormattedReport(): string {
L84: return this.issues.map((issue) => this.formatIssue(issue)).join("\\\\n\\\\n");
L85: }
L86:
L87: private formatIssue(issue: Issue): string {
L88: const location = issue.column
L89: ? `${issue.line}:${issue.column}`
L90: : `line ${issue.line}`;
L91:
L92: let report = `${issue.severity.toUpperCase()}: ${issue.message} (${
L93: issue.nodeType
L94: }) at ${location}`;
L95:
L96: if (issue.source) {
L97: report += `\\\\n${issue.source}`;
L98: }
L99:
L100: if (issue.suggestion) {
L101: report += `\\\\nSuggestion: ${issue.suggestion}`;
L102: }
L103:
L104: return report;
L105: }
L106:
L107: private getSourceSnippet(node: Node): string | undefined {
L108: // Implementation would require access to source code
L109: // Could be injected through constructor
L110: return undefined;
L111: }
L112: }
L113:
L114: /**
L115: * Global issue reporter instance
L116: * Could also be instantiated per validation run if needed
L117: */
L118: export const globalIssueReporter = new IssueReporter();
L119:
L120: /**
L121: * Convenience function for reporting issues
L122: */
L123: export function reportIssue(
L124: node: Node,
L125: message: string,
L126: nodeType: string,
L127: suggestion?: string,
L128: ): void {
L129: globalIssueReporter.reportIssue(
L130: node,
L131: message,
L132: nodeType,
L133: IssueSeverity.ERROR,
L134: suggestion,
L135: );
L136: }
L137:
</src/reporting.ts>
<src/resource-manager.ts>
L1: import type { Node } from "@babel/types";
L2: import { type ValidationConfig, relaxedConfig } from "./types";
L3: /**
L4: * Manages resource usage and enforces limits during validation
L5: * Tracks node count, execution time, and validates against configured limits
L6: */
L7: export class ResourceManager {
L8: private nodeCount = 0;
L9: private startTime: number;
L10: readonly config: ValidationConfig;
L11: private lastTimeoutCheck: number = Date.now();
L12: private readonly CHECK_INTERVAL_MS = 100; // Check every 100ms
L13:
L14: // Track resource usage per validation level to handle nested validations
L15: private readonly depthMap: Map<number, number> = new Map();
L16:
L17: constructor(config: ValidationConfig) {
L18: this.config = config;
L19: this.startTime = Date.now();
L20: }
L21:
L22: /**
L23: * Resets all counters and timers
L24: * Should be called before starting a new validation
L25: */
L26: public reset(): void {
L27: this.nodeCount = 0;
L28: this.startTime = Date.now();
L29: this.depthMap.clear();
L30: }
L31:
L32: /**
L33: * Checks if execution has exceeded configured timeout
L34: * @throws {ValidationError} if timeout is exceeded
L35: */
L36: public checkTimeout(): void {
L37: const elapsed = Date.now() - this.startTime;
L38: if (elapsed > this.config.timeoutMs) {
L39: throw new ValidationError(
L40: `Validation timeout exceeded (${this.config.timeoutMs}ms)`,
L41: "Timeout",
L42: );
L43: }
L44: }
L45:
L46: /**
L47: * Checks if execution has exceeded configured timeout
L48: * @throws {ValidationError} if timeout is exceeded
L49: */
L50: public checkTimeoutAggressive(): void {
L51: const elapsed = Date.now() - this.startTime;
L52: if (elapsed > this.config.timeoutMs * 0.9) {
L53: // 90% of timeout
L54: throw new ValidationError(
L55: `Operation approaching timeout limit`,
L56: "Timeout",
L57: );
L58: }
L59: }
L60:
L61: /**
L62: * Runs an async operation with timeout check
L63: * @param operation - Operation to run
L64: * @returns Result of operation
L65: */
L66: public async withTimeoutCheck<T>(operation: () => Promise<T>): Promise<T> {
L67: this.checkTimeoutAggressive();
L68: const result = await operation();
L69: this.checkTimeout();
L70: return result;
L71: }
L72:
L73: /**
L74: * Runs a synchronous operation with timeout check
L75: * Uses Node.js worker threads for CPU-bound operations
L76: * @param operation - Operation to run
L77: * @returns Result of operation
L78: * @throws {ValidationError} if timeout is exceeded
L79: **/
L80: public withTimeoutCheckSync<T>(operation: () => T): T {
L81: this.checkTimeoutAggressive();
L82: const result = operation();
L83: this.checkTimeout();
L84: return result;
L85: }
L86:
L87: /**
L88: * Increments node count and checks against limit
L89: * @throws {ValidationError} if node count exceeds maximum
L90: */
L91: public incrementNodeCount(): void {
L92: this.nodeCount++;
L93:
L94: // Only check timeout periodically
L95: const now = Date.now();
L96: if (now - this.lastTimeoutCheck > this.CHECK_INTERVAL_MS) {
L97: this.checkTimeout();
L98: this.lastTimeoutCheck = now;
L99: }
L100:
L101: if (this.nodeCount > this.config.maxNodeCount) {
L102: throw new ValidationError(
L103: `Node count exceeded maximum of ${this.config.maxNodeCount}`,
L104: "NodeLimit",
L105: );
L106: }
L107: }
L108:
L109: /**
L110: * Tracks and validates nested depth of validations
L111: * @param depth - Current depth level
L112: * @param type - Type of depth being tracked (e.g., 'object', 'chain')
L113: * @throws {ValidationError} if depth exceeds maximum
L114: */
L115: public trackDepth(
L116: depth: number,
L117: type: "object" | "chain" | "argument",
L118: ): void {
L119: const currentCount = this.depthMap.get(depth) || 0;
L120: this.depthMap.set(depth, currentCount + 1);
L121:
L122: const maxDepth = this.getMaxDepthForType(type);
L123: if (depth > maxDepth) {
L124: throw new ValidationError(
L125: `${type} nesting depth exceeded maximum of ${maxDepth}`,
L126: "DepthLimit",
L127: );
L128: }
L129: }
L130:
L131: /**
L132: * Validates size of a collection (array, object properties, etc.)
L133: * @param size - Size to validate
L134: * @param maxSize - Maximum allowed size
L135: * @param type - Type of collection being validated
L136: * @throws {ValidationError} if size exceeds maximum
L137: */
L138: public validateSize(size: number, maxSize: number, type: string): void {
L139: if (size > maxSize) {
L140: throw new ValidationError(
L141: `${type} size exceeded maximum of ${maxSize}`,
L142: "SizeLimit",
L143: );
L144: }
L145: }
L146:
L147: /**
L148: * Returns current resource usage statistics
L149: */
L150: public getStats(): ResourceStats {
L151: return {
L152: nodeCount: this.nodeCount,
L153: executionTime: Date.now() - this.startTime,
L154: maxDepthReached: Math.max(...this.depthMap.keys(), 0),
L155: };
L156: }
L157:
L158: private getMaxDepthForType(type: "object" | "chain" | "argument"): number {
L159: switch (type) {
L160: case "object":
L161: return this.config.maxObjectDepth;
L162: case "chain":
L163: return this.config.maxChainDepth;
L164: case "argument":
L165: return this.config.maxArgumentNesting;
L166: default:
L167: throw new Error(`Unknown depth type: ${type}`);
L168: }
L169: }
L170: }
L171:
L172: /**
L173: * Custom error class for validation failures
L174: */
L175: export class ValidationError extends Error {
L176: constructor(
L177: message: string,
L178: public readonly type: ValidationErrorType,
L179: public readonly node?: Node,
L180: ) {
L181: super(message);
L182: this.name = "ValidationError";
L183: }
L184: }
L185:
L186: /**
L187: * Types of validation errors that can occur
L188: */
L189: export type ValidationErrorType =
L190: | "Timeout"
L191: | "NodeLimit"
L192: | "DepthLimit"
L193: | "SizeLimit";
L194:
L195: /**
L196: * Statistics about resource usage during validation
L197: */
L198: export interface ResourceStats {
L199: nodeCount: number;
L200: executionTime: number;
L201: maxDepthReached: number;
L202: }
L203:
L204: /**
L205: * Utility class for running operations with timeout
L206: */
L207: export class TimeoutRunner {
L208: constructor(private readonly timeoutMs: number) {}
L209:
L210: /**
L211: * Runs an async operation with timeout
L212: * @param operation - Operation to run
L213: * @returns Result of operation
L214: * @throws {ValidationError} if timeout is exceeded
L215: */
L216: public async runWithTimeout<T>(operation: () => Promise<T>): Promise<T> {
L217: const timeoutPromise = new Promise<never>((_, reject) => {
L218: setTimeout(() => {
L219: reject(
L220: new ValidationError(
L221: `Operation timed out after ${this.timeoutMs}ms`,
L222: "Timeout",
L223: ),
L224: );
L225: }, this.timeoutMs);
L226: });
L227:
L228: return Promise.race([operation(), timeoutPromise]);
L229: }
L230:
L231: /**
L232: * Runs a synchronous operation with timeout
L233: * Uses Node.js worker threads for CPU-bound operations
L234: */
L235: public runSync<T>(operation: () => T): T {
L236: // TODO: Implementation should use Worker threads
L237: throw new Error("Not implemented");
L238: }
L239: }
L240:
L241: /**
L242: * Factory function to create a ResourceManager with optional initial config
L243: */
L244: export function createResourceManager(
L245: config?: Partial<ValidationConfig>,
L246: ): ResourceManager {
L247: return new ResourceManager({
L248: ...relaxedConfig,
L249: ...config,
L250: });
L251: }
L252:
</src/resource-manager.ts>
<src/run.ts>
L1: import * as fs from "fs";
L2: import * as path from "path";
L3: import { stdin as input } from "node:process";
L4: import * as readline from "node:readline";
L5: import clipboardy from "clipboardy";
L6: import { validateZodSchema } from ".";
L7: import { extremelySafeConfig, mediumConfig, relaxedConfig } from "./types";
L8:
L9: interface CliOptions {
L10: stdin: boolean;
L11: clipboard: boolean;
L12: config: "extremelySafe" | "medium" | "relaxed";
L13: cleanOnly: boolean;
L14: json: boolean;
L15: help: boolean;
L16: }
L17:
L18: async function readFromStdin(): Promise<string> {
L19: const rl = readline.createInterface({ input });
L20: const lines: string[] = [];
L21: for await (const line of rl) {
L22: lines.push(line);
L23: }
L24: return lines.join("\\\\n");
L25: }
L26:
L27: function printHelp(): void {
L28: console.log(`Usage: zodsheriff [options] [file]
L29:
L30: Options:
L31: --stdin Read schema from standard input
L32: --clipboard Read schema from system clipboard
L33: --config <level> Set validation config: extremelySafe | medium | relaxed (default: relaxed)
L34: --clean-only Output only the cleaned schema
L35: --json Output result in JSON format
L36: --help Show this help message
L37:
L38: Examples:
L39: zodsheriff schema.ts
L40: zodsheriff --stdin < schema.ts
L41: zodsheriff --clipboard
L42: zodsheriff --config medium schema.ts
L43: zodsheriff --clean-only schema.ts`);
L44: }
L45:
L46: async function readInput(
L47: options: CliOptions,
L48: inputFilePath?: string
L49: ): Promise<string> {
L50: if (options.stdin) {
L51: return readFromStdin();
L52: }
L53: if (options.clipboard) {
L54: return clipboardy.read();
L55: }
L56: if (inputFilePath) {
L57: return fs.readFileSync(path.resolve(inputFilePath), "utf8");
L58: }
L59: throw new Error(
L60: "No input specified. Use --stdin, --clipboard, or provide a file path."
L61: );
L62: }
L63:
L64: async function main() {
L65: const args = process.argv.slice(2);
L66: const options: CliOptions = {
L67: stdin: false,
L68: clipboard: false,
L69: config: "relaxed",
L70: cleanOnly: false,
L71: json: false,
L72: help: false,
L73: };
L74:
L75: let inputFilePath: string | undefined;
L76:
L77: // Parse arguments
L78: for (let i = 0; i < args.length; i++) {
L79: const arg = args[i];
L80: if (arg === "--help") {
L81: printHelp();
L82: process.exit(0);
L83: }
L84: if (arg === "--stdin") {
L85: options.stdin = true;
L86: } else if (arg === "--clipboard") {
L87: options.clipboard = true;
L88: } else if (arg === "--config") {
L89: const val = args[i + 1];
L90: if (!val || !["extremelySafe", "medium", "relaxed"].includes(val)) {
L91: console.error(
L92: "Invalid config value. Must be one of: extremelySafe, medium, relaxed"
L93: );
L94: process.exit(1);
L95: }
L96: options.config = val as CliOptions["config"];
L97: i++;
L98: } else if (arg === "--clean-only") {
L99: options.cleanOnly = true;
L100: } else if (arg === "--json") {
L101: options.json = true;
L102: } else if (!arg.startsWith("--")) {
L103: inputFilePath = arg;
L104: }
L105: }
L106:
L107: try {
L108: const schemaCode = await readInput(options, inputFilePath);
L109: const configMap = {
L110: extremelySafe: extremelySafeConfig,
L111: medium: mediumConfig,
L112: relaxed: relaxedConfig,
L113: };
L114:
L115: const result = await validateZodSchema(
L116: schemaCode,
L117: configMap[options.config]
L118: );
L119:
L120: if (options.json) {
L121: console.log(JSON.stringify(result, null, 2));
L122: process.exit(result.isValid ? 0 : 1);
L123: }
L124:
L125: if (options.cleanOnly) {
L126: if (result.cleanedCode) {
L127: console.log(result.cleanedCode);
L128: process.exit(0);
L129: }
L130: console.error("No cleaned code generated due to validation errors.");
L131: process.exit(1);
L132: }
L133:
L134: // Standard output
L135: if (!result.isValid) {
L136: console.log("❌ Validation failed.");
L137: if (result.issues.length > 0) {
L138: console.log("Issues:");
L139: for (const issue of result.issues) {
L140: console.log(
L141: `- ${issue.severity.toUpperCase()}: ${issue.message} (at line ${
L142: issue.line
L143: }, node: ${issue.nodeType})`
L144: );
L145: }
L146: }
L147: } else {
L148: console.log("✅ Validation passed.");
L149: }
L150:
L151: if (result.cleanedCode) {
L152: console.log("Cleaned schema:");
L153: console.log(result.cleanedCode);
L154: }
L155:
L156: process.exit(result.isValid ? 0 : 1);
L157: } catch (err) {
L158: console.error("Error:", err instanceof Error ? err.message : err);
L159: process.exit(1);
L160: }
L161: }
L162:
L163: main().catch((err) => {
L164: console.error("Fatal error:", err instanceof Error ? err.message : err);
L165: process.exit(1);
L166: });
L167:
</src/run.ts>
<src/schema-validator.ts>
L1: import { parse } from "@babel/parser";
L2: import _traverse from "@babel/traverse";
L3: import _generate from "@babel/generator";
L4: import {
L5: type Node,
L6: type File,
L7: type Statement,
L8: type Expression,
L9: type VariableDeclaration,
L10: type VariableDeclarator,
L11: exportNamedDeclaration,
L12: } from "@babel/types";
L13: import type { ValidationConfig } from "./types";
L14: import { ResourceManager } from "./resource-manager";
L15: import { IssueReporter, IssueSeverity } from "./reporting";
L16: import { ChainValidator } from "./chain-validator";
L17: import { ArgumentValidator } from "./argument-validator";
L18:
L19: // Handle ESM default export
L20: const traverse = (_traverse as any).default || _traverse;
L21: const generate = (_generate as any).default || _generate;
L22:
L23: /**
L24: * SchemaValidator class
L25: * Main class responsible for validating Zod schema definitions.
L26: * Coordinates validation of imports, schema structure, and method chains
L27: * while enforcing configured safety limits.
L28: */
L29: export class SchemaValidator {
L30: private readonly resourceManager: ResourceManager;
L31: private readonly issueReporter: IssueReporter;
L32: private readonly chainValidator: ChainValidator;
L33: private readonly argumentValidator: ArgumentValidator;
L34:
L35: constructor(
L36: private readonly config: ValidationConfig,
L37: resourceManager?: ResourceManager,
L38: issueReporter?: IssueReporter
L39: ) {
L40: this.resourceManager = resourceManager ?? new ResourceManager(config);
L41: this.issueReporter = issueReporter ?? new IssueReporter();
L42: this.chainValidator = new ChainValidator(
L43: config,
L44: this.resourceManager,
L45: this.issueReporter
L46: );
L47: this.argumentValidator = new ArgumentValidator(
L48: config,
L49: this.resourceManager,
L50: this.issueReporter
L51: );
L52: }
L53:
L54: /**
L55: * Main schema validation method
L56: *
L57: * Process:
L58: * 1. Parses input code to AST
L59: * 2. Validates imports
L60: * 3. Processes and validates declarations
L61: * 4. Removes invalid nodes
L62: * 5. Auto-exports valid schemas
L63: * 6. Generates cleaned output
L64: *
L65: * Notes:
L66: * - Returns isValid: false if any errors are found
L67: * - Still returns cleaned code containing valid schemas
L68: * - Reports all validation issues found
L69: *
L70: * @param schemaCode - The schema code to validate
L71: * @returns Promise<ValidationResult> with validation status, cleaned code, and issues
L72: */
L73: public async validateSchema(schemaCode: string): Promise<ValidationResult> {
L74: this.resourceManager.reset();
L75: this.issueReporter.clear();
L76:
L77: try {
L78: // Parse the code
L79: const ast = await this.parseCode(schemaCode);
L80: if (!ast) {
L81: return {
L82: isValid: false,
L83: cleanedCode: "",
L84: issues: this.issueReporter.getIssues(),
L85: };
L86: }
L87:
L88: let hasValidSchemas = false;
L89: let hasErrors = false;
L90: const nodesToRemove = new Set<Node>();
L91:
L92: // First check for required zod import
L93: const hasZodImport = this.validateZodImport(ast);
L94: if (!hasZodImport) {
L95: hasErrors = true;
L96: }
L97:
L98: // Traverse and validate the AST
L99: traverse(ast, {
L100: ImportDeclaration: (path) => {
L101: if (path.node.source.value !== "zod") {
L102: this.issueReporter.reportIssue(
L103: path.node,
L104: `Invalid import from '${path.node.source.value}'. Only 'zod' imports are allowed.`,
L105: "ImportDeclaration",
L106: IssueSeverity.ERROR
L107: );
L108: nodesToRemove.add(path.node);
L109: hasErrors = true;
L110: }
L111: },
L112:
L113: VariableDeclaration: (path) => {
L114: const isValid = this.validateVariableDeclaration(path.node);
L115:
L116: if (!isValid) {
L117: nodesToRemove.add(path.node);
L118: hasErrors = true;
L119: } else {
L120: // Check if this contains any schema declarations
L121: const hasSchema = path.node.declarations.some((decl) =>
L122: this.isSchemaDeclaration(decl)
L123: );
L124: if (hasSchema) {
L125: hasValidSchemas = true;
L126: // If it's valid and not already exported, wrap it in an export
L127: if (
L128: !path.parent ||
L129: path.parent.type !== "ExportNamedDeclaration"
L130: ) {
L131: const exportDecl = exportNamedDeclaration(path.node, []);
L132: path.replaceWith(exportDecl);
L133: }
L134: } else {
L135: nodesToRemove.add(path.node);
L136: }
L137: }
L138: },
L139:
L140: Statement: (path) => {
L141: if (!this.isAllowedStatement(path.node)) {
L142: this.issueReporter.reportIssue(
L143: path.node,
L144: `Invalid statement type: ${path.node.type}`,
L145: path.node.type,
L146: IssueSeverity.ERROR
L147: );
L148: nodesToRemove.add(path.node);
L149: hasErrors = true;
L150: }
L151: },
L152: });
L153:
L154: // Remove invalid nodes
L155: traverse(ast, {
L156: enter(path) {
L157: if (nodesToRemove.has(path.node)) {
L158: path.remove();
L159: }
L160: },
L161: });
L162:
L163: // Generate cleaned code if we found any valid schemas
L164: let cleanedCode = "";
L165: if (hasValidSchemas) {
L166: const generated = generate(ast, {
L167: comments: true,
L168: compact: false,
L169: });
L170: cleanedCode = generated.code;
L171: }
L172:
L173: return {
L174: isValid: !hasErrors,
L175: cleanedCode,
L176: issues: this.issueReporter.getIssues(),
L177: };
L178: } catch (error) {
L179: this.handleError(error);
L180: return {
L181: isValid: false,
L182: cleanedCode: "",
L183: issues: this.issueReporter.getIssues(),
L184: };
L185: }
L186: }
L187:
L188: /**
L189: * Validates that the code properly imports 'z' from 'zod'
L190: *
L191: * Checks for:
L192: * - Presence of zod import
L193: * - Correct import specifier ('z')
L194: * - No other imports from other modules
L195: *
L196: * @param ast - The AST to validate
L197: * @returns boolean indicating if import is valid
L198: */
L199: private validateZodImport(ast: File): boolean {
L200: const hasZodImport = ast.program.body.some((node) => {
L201: if (node.type !== "ImportDeclaration") return false;
L202: if (node.source.value !== "zod") return false;
L203:
L204: return node.specifiers.some((spec) => {
L205: return (
L206: (spec.type === "ImportDefaultSpecifier" ||
L207: spec.type === "ImportSpecifier") &&
L208: spec.local.name === "z"
L209: );
L210: });
L211: });
L212:
L213: if (!hasZodImport) {
L214: this.issueReporter.reportIssue(
L215: { type: "File", loc: { start: { line: 1, column: 0 } } } as Node,
L216: "Missing 'z' import from 'zod'",
L217: "File",
L218: IssueSeverity.ERROR
L219: );
L220: }
L221:
L222: return hasZodImport;
L223: }
L224:
L225: /**
L226: * Validates a variable declaration node to ensure it meets schema requirements
L227: *
L228: * Validates that:
L229: * - Declaration uses 'const'
L230: * - Has a proper initializer (not undefined or missing)
L231: * - Schema initialization is valid
L232: *
L233: * @param node - The variable declaration to validate
L234: * @returns boolean indicating if the declaration is valid
L235: */
L236: private validateVariableDeclaration(node: VariableDeclaration): boolean {
L237: // Only allow const declarations
L238: if (node.kind !== "const") {
L239: this.issueReporter.reportIssue(
L240: node,
L241: "Schema declarations must use 'const'",
L242: "VariableDeclaration",
L243: IssueSeverity.ERROR
L244: );
L245: return false;
L246: }
L247:
L248: let isValid = true;
L249: for (const declarator of node.declarations) {
L250: // Check for missing initializer
L251: if (!declarator.init) {
L252: this.issueReporter.reportIssue(
L253: declarator,
L254: "Schema declaration must have an initializer",
L255: "VariableDeclarator",
L256: IssueSeverity.ERROR
L257: );
L258: isValid = false;
L259: continue;
L260: }
L261:
L262: // Check for undefined initializer
L263: if (
L264: declarator.init.type === "Identifier" &&
L265: declarator.init.name === "undefined"
L266: ) {
L267: this.issueReporter.reportIssue(
L268: declarator,
L269: "Schema declaration must have an initializer",
L270: "VariableDeclarator",
L271: IssueSeverity.ERROR
L272: );
L273: isValid = false;
L274: continue;
L275: }
L276:
L277: // For schema declarations, validate the initialization
L278: if (this.isSchemaDeclaration(declarator)) {
L279: if (!this.validateSchemaExpression(declarator.init)) {
L280: isValid = false;
L281: }
L282: }
L283: }
L284:
L285: return isValid;
L286: }
L287:
L288: /**
L289: * Determines if a variable declarator represents a schema declaration
L290: *
L291: * Checks:
L292: * - Variable name (contains 'schema')
L293: * - Initialization pattern (z.* or schema-like call expression)
L294: *
L295: * @param declarator - The variable declarator to check
L296: * @returns boolean indicating if this is a schema declaration
L297: */
L298: private isSchemaDeclaration(declarator: VariableDeclarator): boolean {
L299: if (!declarator.init) return false;
L300:
L301: // Check for explicit schema naming
L302: if (
L303: declarator.id.type === "Identifier" &&
L304: declarator.id.name.toLowerCase().includes("schema")
L305: ) {
L306: return true;
L307: }
L308:
L309: // Check initialization pattern
L310: const init = declarator.init;
L311: return (
L312: init.type === "CallExpression" ||
L313: (init.type === "MemberExpression" &&
L314: init.object.type === "Identifier" &&
L315: init.object.name === "z")
L316: );
L317: }
L318:
L319: /**
L320: * Parses input code into an AST
L321: *
L322: * @param code - The code to parse
L323: * @returns Promise<File | null> The parsed AST or null if parsing fails
L324: */
L325: private async parseCode(code: string): Promise<File | null> {
L326: try {
L327: return parse(code.trim(), {
L328: sourceType: "module",
L329: plugins: ["typescript"],
L330: tokens: true,
L331: });
L332: } catch (error) {
L333: this.issueReporter.reportIssue(
L334: { type: "File", loc: { start: { line: 1, column: 0 } } } as Node,
L335: `Failed to parse schema: ${
L336: error instanceof Error ? error.message : "Unknown error"
L337: }`,
L338: "File",
L339: IssueSeverity.ERROR
L340: );
L341: return null;
L342: }
L343: }
L344:
L345: /**
L346: * Validates a schema expression
L347: * Currently delegates to chain validator for method chain validation
L348: *
L349: * @param node - The expression to validate
L350: * @returns boolean indicating if the expression is valid
L351: */
L352: private validateSchemaExpression(node: Expression): boolean {
L353: return this.chainValidator.validateChain(node);
L354: }
L355:
L356: /**
L357: * Checks if a statement type is allowed in schema definitions
L358: *
L359: * @param node - The statement to check
L360: * @returns boolean indicating if the statement type is allowed
L361: */
L362: private isAllowedStatement(node: Statement): boolean {
L363: return (
L364: node.type === "ImportDeclaration" ||
L365: node.type === "ExportNamedDeclaration" ||
L366: node.type === "VariableDeclaration" ||
L367: node.type === "ExportDefaultDeclaration"
L368: );
L369: }
L370:
L371: /**
L372: * Handles errors during validation
L373: * Converts errors to validation issues
L374: *
L375: * @param error - The error to handle
L376: */
L377: private handleError(error: unknown): void {
L378: const message = error instanceof Error ? error.message : "Unknown error";
L379: this.issueReporter.reportIssue(
L380: { type: "File", loc: { start: { line: 1, column: 0 } } } as Node,
L381: `Validation error: ${message}`,
L382: "File",
L383: IssueSeverity.ERROR
L384: );
L385: }
L386: }
L387:
L388: /**
L389: * Result of schema validation
L390: */
L391: interface ValidationResult {
L392: /** Whether the schema is valid (no errors found) */
L393: isValid: boolean;
L394: /** The cleaned and formatted schema code */
L395: cleanedCode: string;
L396: /** Array of validation issues found */
L397: issues: Array<{
L398: line: number;
L399: column?: number;
L400: message: string;
L401: nodeType: string;
L402: severity: IssueSeverity;
L403: suggestion?: string;
L404: }>;
L405: }
L406:
L407: /**
L408: * Convenience function to validate a schema
L409: *
L410: * @param schemaCode - The schema code to validate
L411: * @param config - The validation configuration to use
L412: * @returns Promise<ValidationResult>
L413: */
L414: export async function validateSchema(
L415: schemaCode: string,
L416: config: ValidationConfig
L417: ): Promise<ValidationResult> {
L418: const validator = new SchemaValidator(config);
L419: return validator.validateSchema(schemaCode);
L420: }
L421:
</src/schema-validator.ts>
<src/types.ts>
L1: /**
L2: * Core configuration interface for validation settings
L3: */
L4: export interface ValidationConfig {
L5: // Timeout settings
L6: timeoutMs: number;
L7:
L8: // Resource limits
L9: maxNodeCount: number;
L10: maxObjectDepth: number;
L11: maxChainDepth: number;
L12: maxArgumentNesting: number;
L13: maxPropertiesPerObject: number;
L14: maxStringLength: number;
L15:
L16: // Performance settings
L17: enableParallelProcessing: boolean;
L18: maxConcurrentValidations: number;
L19: enableCaching: boolean;
L20:
L21: // Safety settings
L22: allowLoops: boolean;
L23: allowComputedProperties: boolean;
L24: allowTemplateExpressions: boolean;
L25: propertySafety: PropertySafetyConfig;
L26:
L27: // Runtime checks
L28: addRuntimeProtection: boolean;
L29: }
L30:
L31: /**
L32: * Configuration for property name safety checks
L33: */
L34: export interface PropertySafetyConfig {
L35: allowedPrefixes: string[];
L36: deniedPrefixes: string[];
L37: allowedProperties: Set<string>;
L38: deniedProperties: Set<string>;
L39: }
L40:
L41: /**
L42: * Predefined validation configurations
L43: */
L44: export const extremelySafeConfig: ValidationConfig = {
L45: timeoutMs: 1000,
L46: maxNodeCount: 1000,
L47: maxObjectDepth: 3,
L48: maxChainDepth: 3,
L49: maxArgumentNesting: 2,
L50: maxPropertiesPerObject: 20,
L51: maxStringLength: 100,
L52: enableParallelProcessing: false,
L53: maxConcurrentValidations: 1,
L54: enableCaching: true,
L55: allowLoops: false,
L56: allowComputedProperties: false,
L57: allowTemplateExpressions: false,
L58: propertySafety: {
L59: allowedPrefixes: [],
L60: deniedPrefixes: ["_", "$"],
L61: allowedProperties: new Set(["type", "value", "items"]),
L62: deniedProperties: new Set(["__proto__", "constructor", "prototype"]),
L63: },
L64: addRuntimeProtection: true,
L65: };
L66:
L67: export const mediumConfig: ValidationConfig = {
L68: timeoutMs: 5000, // 5 seconds
L69: maxNodeCount: 10000,
L70: maxObjectDepth: 5,
L71: maxChainDepth: 5,
L72: maxArgumentNesting: 4,
L73: maxPropertiesPerObject: 100,
L74: maxStringLength: 1000,
L75: enableParallelProcessing: true,
L76: maxConcurrentValidations: 4,
L77: enableCaching: true,
L78: allowLoops: true,
L79: allowComputedProperties: false,
L80: allowTemplateExpressions: true,
L81: propertySafety: {
L82: allowedPrefixes: [],
L83: deniedPrefixes: ["__"], // Only block double underscore
L84: allowedProperties: new Set(), // Empty = allow all except denied
L85: deniedProperties: new Set([
L86: "__proto__",
L87: "constructor",
L88: "prototype",
L89: "eval",
L90: "arguments",
L91: "process",
L92: "global",
L93: "window",
L94: "document",
L95: ]),
L96: },
L97: addRuntimeProtection: true,
L98: };
L99:
L100: export const relaxedConfig: ValidationConfig = {
L101: timeoutMs: 30000, // 30 seconds
L102: maxNodeCount: 1000000,
L103: maxObjectDepth: 10,
L104: maxChainDepth: 10,
L105: maxArgumentNesting: 8,
L106: maxPropertiesPerObject: 1000,
L107: maxStringLength: 10000,
L108: enableParallelProcessing: true,
L109: maxConcurrentValidations: 8,
L110: enableCaching: true,
L111: allowLoops: true,
L112: allowComputedProperties: true,
L113: allowTemplateExpressions: true,
L114: propertySafety: {
L115: allowedPrefixes: [], // Allow all prefixes
L116: deniedPrefixes: ["__"], // Still block double underscore for safety
L117: allowedProperties: new Set(), // Empty = allow all except denied
L118: deniedProperties: new Set([
L119: "__proto__",
L120: "constructor", // Minimal safety - just block prototype pollution
L121: ]),
L122: },
L123: addRuntimeProtection: false, // Trust the code more in relaxed mode
L124: };
L125:
L126: // Helper to combine configs with overrides
L127: export function createConfig(
L128: baseConfig: ValidationConfig,
L129: overrides?: Partial<ValidationConfig>,
L130: ): ValidationConfig {
L131: return {
L132: ...baseConfig,
L133: ...overrides,
L134: // Deep merge for nested objects
L135: propertySafety: {
L136: ...baseConfig.propertySafety,
L137: ...overrides?.propertySafety,
L138: // Ensure Sets are properly merged
L139: allowedProperties: new Set([
L140: ...Array.from(baseConfig.propertySafety.allowedProperties),
L141: ...(overrides?.propertySafety?.allowedProperties || []),
L142: ]),
L143: deniedProperties: new Set([
L144: ...Array.from(baseConfig.propertySafety.deniedProperties),
L145: ...(overrides?.propertySafety?.deniedProperties || []),
L146: ]),
L147: },
L148: };
L149: }
L150:
L151: /**
L152: * Location information for nodes
L153: */
L154: export interface Location {
L155: line: number;
L156: column: number;
L157: }
L158:
L159: /**
L160: * Base interface for all validation context
L161: */
L162: export interface ValidationContext {
L163: config: ValidationConfig;
L164: parentNodes: Node[];
L165: depth: number;
L166: }
L167:
</src/types.ts>
<src/zod-method-names.ts>
L1: export const allowedZodMethods = new Set([
L2: // Primitives
L3: "string",
L4: "number",
L5: "boolean",
L6: "date",
L7: "bigint",
L8: "symbol",
L9:
L10: // Empty types
L11: "undefined",
L12: "null",
L13: "void",
L14:
L15: // Catch-all types
L16: "any",
L17: "unknown",
L18: "never",
L19:
L20: // Complex types
L21: "array",
L22: "object",
L23: "union",
L24: "discriminatedUnion",
L25: "intersection",
L26: "tuple",
L27: "record",
L28: "map",
L29: "set",
L30: "function",
L31: "promise",
L32:
L33: // Special types
L34: "enum",
L35: "nativeEnum",
L36: "literal",
L37: "lazy",
L38:
L39: // Coercion
L40: "coerce",
L41:
L42: // Effects
L43: "optional",
L44: "nullable",
L45: "nullish",
L46: "transform",
L47: "default",
L48: "catch",
L49: "preprocess",
L50:
L51: // Custom
L52: "custom",
L53:
L54: // Type helpers
L55: "instanceof",
L56: ]);
L57:
L58: export const allowedChainMethods = new Set([
L59: // String specific validations
L60: "min",
L61: "max",
L62: "length",
L63: "email",
L64: "url",
L65: "emoji",
L66: "uuid",
L67: "cuid",
L68: "cuid2",
L69: "ulid",
L70: "regex",
L71: "includes",
L72: "startsWith",
L73: "endsWith",
L74: "datetime",
L75: "ip",
L76: "cidr",
L77: "trim",
L78: "toLowerCase",
L79: "toUpperCase",
L80: "date",
L81: "time",
L82: "duration",
L83: "base64",
L84: "nanoid",
L85:
L86: // Number specific validations
L87: "gt",
L88: "gte",
L89: "lt",
L90: "lte",
L91: "int",
L92: "positive",
L93: "negative",
L94: "nonpositive",
L95: "nonnegative",
L96: "multipleOf",
L97: "finite",
L98: "safe",
L99:
L100: // Array/Set methods
L101: "nonempty",
L102: "size",
L103: "element",
L104:
L105: // Effects and transforms
L106: "optional",
L107: "nullable",
L108: "nullish",
L109: "transform",
L110: "default",
L111: "catch",
L112: "preprocess",
L113: "refine",
L114: "superRefine",
L115: "pipe",
L116: "brand",
L117: "readonly",
L118:
L119: // Object methods
L120: "partial",
L121: "deepPartial",
L122: "required",
L123: "passthrough",
L124: "strict",
L125: "strip",
L126: "catchall",
L127: "pick",
L128: "omit",
L129: "extend",
L130: "merge",
L131: "keyof",
L132: "shape",
L133:
L134: // Common operations
L135: "describe",
L136: "or",
L137: "and",
L138:
L139: // Type conversions
L140: "array",
L141: "promise",
L142: ]);
L143:
</src/zod-method-names.ts>
<tests/argument-validator.test.ts>
L1: import { ArgumentValidator } from "../src/argument-validator";
L2: import {
L3: TestResourceManager,
L4: createTestConfig,
L5: parseCallExpression,
L6: } from "./test-utils";
L7: import { parse } from "@babel/parser";
L8: import { CallExpression } from "@babel/types";
L9:
L10: describe("ArgumentValidator", () => {
L11: let validator: ArgumentValidator;
L12: let resourceManager: TestResourceManager;
L13:
L14: beforeEach(() => {
L15: const config = createTestConfig();
L16: resourceManager = new TestResourceManager(config);
L17: validator = new ArgumentValidator(config, resourceManager);
L18: });
L19:
L20: function parseAndValidate(code: string, methodName: string): boolean {
L21: const ast = parse(code);
L22: const stmt = ast.program.body[0];
L23: if (stmt.type !== "ExpressionStatement")
L24: throw new Error("Expected expression statement");
L25: const expr = stmt.expression;
L26: if (expr.type !== "CallExpression")
L27: throw new Error("Expected call expression");
L28: return validator.validateMethodArguments(expr, methodName);
L29: }
L30:
L31: describe("validateMethodArguments", () => {
L32: it("should validate refine method arguments with a proper function", () => {
L33: const result = parseAndValidate(
L34: `schema.refine((val) => val > 0)`,
L35: "refine"
L36: );
L37: expect(result).toBe(true);
L38: });
L39:
L40: it("should reject refine method arguments if function is async", () => {
L41: const result = parseAndValidate(
L42: `schema.refine(async (val) => val > 0)`,
L43: "refine"
L44: );
L45: expect(result).toBe(false);
L46: });
L47:
L48: it("should reject refine method arguments if function is a generator", () => {
L49: const result = parseAndValidate(
L50: `schema.refine(function* (val) { yield val; })`,
L51: "refine"
L52: );
L53: expect(result).toBe(false);
L54: });
L55:
L56: it("should validate transform method arguments with a simple synchronous function", () => {
L57: const result = parseAndValidate(
L58: `schema.transform(val => val.toString())`,
L59: "transform"
L60: );
L61: expect(result).toBe(true);
L62: });
L63:
L64: it("should validate pipe method arguments if it’s a schema call", () => {
L65: // Assume pipe allows schema as argument (as per rules)
L66: const result = parseAndValidate(`schema.pipe(z.string())`, "pipe");
L67: expect(result).toBe(true);
L68: });
L69:
L70: it("should reject pipe method arguments if it’s a function (not allowed for pipe)", () => {
L71: const result = parseAndValidate(`schema.pipe((val) => val)`, "pipe");
L72: expect(result).toBe(false);
L73: });
L74:
L75: it("should validate regex method arguments with a safe regex", () => {
L76: const result = parseAndValidate(`schema.regex(/^[a-z]+$/)`, "regex");
L77: expect(result).toBe(true);
L78: });
L79:
L80: it("should reject regex method arguments if pattern is not safe", () => {
L81: // Example of a catastrophic regex
L82: const result = parseAndValidate(`schema.regex(/^(a+)+$/)`, "regex");
L83: expect(result).toBe(false);
L84: });
L85:
L86: it("should allow a literal argument (e.g., number) for methods with no specific restrictions", () => {
L87: // For an unknown method with no rules, we just return true
L88: const callExpr = parseCallExpression(`schema.unknownMethod(42)`);
L89: const result = validator.validateMethodArguments(
L90: callExpr,
L91: "unknownMethod"
L92: );
L93: // No rules for unknownMethod, should return true per current logic
L94: expect(result).toBe(true);
L95: });
L96:
L97: it("should reject too many arguments for transform (max 1)", () => {
L98: const result = parseAndValidate(
L99: `schema.transform(val => val, "extra")`,
L100: "transform"
L101: );
L102: expect(result).toBe(false);
L103: });
L104:
L105: it("should reject too few arguments for refine (min 1)", () => {
L106: const result = parseAndValidate(`schema.refine()`, "refine");
L107: expect(result).toBe(false);
L108: });
L109: });
L110: });
L111:
</tests/argument-validator.test.ts>
<tests/chain-validator.test.ts>
L1: import { ChainValidator } from "../src/chain-validator";
L2: import { TestResourceManager, createTestConfig } from "./test-utils";
L3: import { parse } from "@babel/parser";
L4: import { ExpressionStatement, Node } from "@babel/types";
L5: import { IssueReporter } from "../src/reporting";
L6: import { ArgumentValidator } from "../src/argument-validator";
L7:
L8: describe("ChainValidator", () => {
L9: let validator: ChainValidator;
L10: let resourceManager: TestResourceManager;
L11: let issueReporter: IssueReporter;
L12: let argumentValidator: ArgumentValidator;
L13:
L14: beforeEach(() => {
L15: const config = createTestConfig();
L16: resourceManager = new TestResourceManager(config);
L17: issueReporter = new IssueReporter();
L18: argumentValidator = new ArgumentValidator(
L19: config,
L20: resourceManager,
L21: issueReporter
L22: );
L23: validator = new ChainValidator(
L24: config,
L25: resourceManager,
L26: issueReporter,
L27: argumentValidator
L28: );
L29: });
L30:
L31: function parseExpression(code: string): Node {
L32: const ast = parse(code, { sourceType: "module", plugins: ["typescript"] });
L33: const stmt = ast.program.body[0] as ExpressionStatement;
L34: return stmt.expression;
L35: }
L36:
L37: it("should validate a simple chain starting with z", () => {
L38: const node = parseExpression(`z.string()`);
L39: const result = validator.validateChain(node);
L40: expect(result).toBe(true);
L41: expect(issueReporter.getIssues()).toHaveLength(0);
L42: });
L43:
L44: it("should fail if chain does not start with z identifier", () => {
L45: const node = parseExpression(`x.string()`);
L46: const result = validator.validateChain(node);
L47: expect(result).toBe(false);
L48: const issues = issueReporter.getIssues();
L49: expect(issues).toHaveLength(1);
L50: expect(issues[0].message).toContain("Chain must start with 'z'");
L51: });
L52:
L53: it("should validate allowed zod methods in chain", () => {
L54: const node = parseExpression(`z.string().min(5).max(10)`);
L55: const result = validator.validateChain(node);
L56: expect(result).toBe(true);
L57: expect(issueReporter.getIssues()).toHaveLength(0);
L58: });
L59:
L60: it("should report issue for not allowed method in chain", () => {
L61: const node = parseExpression(`z.string().someForbiddenMethod()`);
L62: const result = validator.validateChain(node);
L63: expect(result).toBe(false);
L64: const issues = issueReporter.getIssues();
L65: expect(issues).toHaveLength(1);
L66: expect(issues[0].message).toContain("Method not allowed in chain");
L67: });
L68:
L69: it("should handle deeply chained calls within maxChainDepth", () => {
L70: // Allowed depth is 10 in relaxed config, try a chain of length 5
L71: const node = parseExpression(`z.string().min(1).max(2).trim().email()`);
L72: const result = validator.validateChain(node);
L73: expect(result).toBe(true);
L74: });
L75:
L76: it("should fail if chain depth exceeds maxChainDepth", () => {
L77: // Set a lower maxChainDepth
L78: const config = createTestConfig({ maxChainDepth: 2 });
L79: validator = new ChainValidator(
L80: config,
L81: resourceManager,
L82: issueReporter,
L83: argumentValidator
L84: );
L85:
L86: const node = parseExpression(`z.string().min(1).max(2).trim()`);
L87: const result = validator.validateChain(node);
L88: expect(result).toBe(false);
L89:
L90: const issues = issueReporter.getIssues();
L91: expect(
L92: issues.some((issue) =>
L93: issue.message.includes("Chain nesting depth exceeded")
L94: )
L95: ).toBe(true);
L96: });
L97:
L98: it("should report error for computed member expression properties", () => {
L99: const node = parseExpression(`z.string()[methodName]()`);
L100: const result = validator.validateChain(node);
L101: expect(result).toBe(false);
L102: const issues = issueReporter.getIssues();
L103: expect(issues[0].message).toContain(
L104: "Computed properties not allowed in chain"
L105: );
L106: });
L107: });
L108:
</tests/chain-validator.test.ts>
<tests/complex.test.ts>
L1: import { parse } from "@babel/parser";
L2: import { ExpressionStatement, Node } from "@babel/types";
L3: import { SchemaValidator } from "../src/schema-validator";
L4: import { validateObjectExpression } from "../src/object-validator";
L5: import { ChainValidator } from "../src/chain-validator";
L6: import { ArgumentValidator } from "../src/argument-validator";
L7: import { IssueReporter } from "../src/reporting";
L8: import { createTestConfig, TestResourceManager } from "./test-utils";
L9:
L10: function parseExpression(code: string): Node {
L11: const ast = parse(code, { sourceType: "module", plugins: ["typescript"] });
L12: const stmt = ast.program.body[0] as ExpressionStatement;
L13: if (!stmt || stmt.type !== "ExpressionStatement") {
L14: throw new Error("Expected an expression statement");
L15: }
L16: return stmt.expression;
L17: }
L18:
L19: describe("Complex Validation Tests", () => {
L20: let config: ReturnType<typeof createTestConfig>;
L21: let resourceManager: TestResourceManager;
L22: let issueReporter: IssueReporter;
L23: let chainValidator: ChainValidator;
L24: let argumentValidator: ArgumentValidator;
L25: let schemaValidator: SchemaValidator;
L26:
L27: beforeEach(() => {
L28: config = createTestConfig();
L29: resourceManager = new TestResourceManager(config);
L30: issueReporter = new IssueReporter();
L31: argumentValidator = new ArgumentValidator(
L32: config,
L33: resourceManager,
L34: issueReporter
L35: );
L36: chainValidator = new ChainValidator(
L37: config,
L38: resourceManager,
L39: issueReporter,
L40: argumentValidator
L41: );
L42: schemaValidator = new SchemaValidator(
L43: config,
L44: resourceManager,
L45: issueReporter
L46: );
L47: });
L48:
L49: function parseAndValidate(code: string, methodName: string): boolean {
L50: const ast = parse(code);
L51: const stmt = ast.program.body[0];
L52: if (stmt.type !== "ExpressionStatement")
L53: throw new Error("Expected expression statement");
L54: const expr = stmt.expression;
L55: if (expr.type !== "CallExpression")
L56: throw new Error("Expected call expression");
L57: return argumentValidator.validateMethodArguments(expr, methodName);
L58: }
L59:
L60: // -----------------------
L61: // Argument Validation Tests
L62: // -----------------------
L63: describe("ArgumentValidator - complex scenarios", () => {
L64: it("should validate refine with two arguments and a complex function body", () => {
L65: const code = `
L66: schema.refine(
L67: function(val) {
L68: const result = val > 0;
L69: return result;
L70: },
L71: { message: "Value must be positive" }
L72: );
L73: `;
L74: const result = parseAndValidate(code, "refine");
L75: expect(result).toBe(true);
L76: });
L77:
L78: it("should validate a transform function with nested internal calls", () => {
L79: const code = `
L80: schema.transform((val) => {
L81: function helper(x) { return x.toString().toUpperCase(); }
L82: return helper(val);
L83: })
L84: `;
L85: const result = parseAndValidate(code, "transform");
L86: expect(result).toBe(true);
L87: });
L88:
L89: it("should reject a complex unsafe regex with flags", () => {
L90: const code = `schema.regex(/^(a+)+$/mi)`;
L91: const result = parseAndValidate(code, "regex");
L92: expect(result).toBe(false);
L93: });
L94:
L95: it("should fail if refine is given a non-function (object) argument", () => {
L96: const code = `
L97: schema.refine({
L98: notAFunction: true
L99: });
L100: `;
L101: // 'refine' expects a function, not an object.
L102: const result = parseAndValidate(code, "refine");
L103: expect(result).toBe(false);
L104: });
L105: });
L106:
L107: // -----------------------
L108: // Chain Validation Tests
L109: // -----------------------
L110: describe("ChainValidator - complex scenarios", () => {
L111: it("should detect a disallowed method at the end of a long allowed chain", () => {
L112: const node = parseExpression(
L113: `z.string().min(1).max(10).someForbiddenMethod()`
L114: );
L115: const result = chainValidator.validateChain(node);
L116: expect(result).toBe(false);
L117: const issues = issueReporter.getIssues();
L118: expect(issues[0].message).toContain("Method not allowed in chain");
L119: });
L120:
L121: it("should reject complex member expressions with computed properties deep in chain", () => {
L122: const node = parseExpression(`z.object()[dynamicProp].shape()`);
L123: const result = chainValidator.validateChain(node);
L124: expect(result).toBe(false);
L125: expect(issueReporter.getIssues()[0].message).toContain(
L126: "Computed properties not allowed in chain"
L127: );
L128: });
L129:
L130: it("should fail if chain depth exceeded with multiple valid methods", () => {
L131: const shallowConfig = createTestConfig({ maxChainDepth: 2 });
L132: const shallowResourceManager = new TestResourceManager(shallowConfig);
L133: const shallowIssueReporter = new IssueReporter();
L134: const shallowArgumentValidator = new ArgumentValidator(
L135: shallowConfig,
L136: shallowResourceManager,
L137: shallowIssueReporter
L138: );
L139: const shallowChainValidator = new ChainValidator(
L140: shallowConfig,
L141: shallowResourceManager,
L142: shallowIssueReporter,
L143: shallowArgumentValidator
L144: );
L145: const node = parseExpression(`z.string().min(1).max(2).trim().email()`);
L146: const result = shallowChainValidator.validateChain(node);
L147: expect(result).toBe(false);
L148: expect(
L149: shallowIssueReporter
L150: .getIssues()
L151: .some((issue) =>
L152: issue.message.includes("Chain nesting depth exceeded")
L153: )
L154: ).toBe(true);
L155: });
L156: });
L157:
L158: // -----------------------
L159: // Object Validation Tests
L160: // -----------------------
L161: describe("ObjectValidator - complex scenarios", () => {
L162: function parseObjectExpression(code: string) {
L163: const ast = parse(code);
L164: const stmt = ast.program.body[0] as ExpressionStatement;
L165: if (!stmt || stmt.type !== "ExpressionStatement") {
L166: throw new Error("Expected expression statement");
L167: }
L168: const expr = stmt.expression;
L169: if (expr.type !== "ObjectExpression") {
L170: throw new Error("Expected object expression");
L171: }
L172: return expr;
L173: }
L174:
L175: it("should detect unsafe property at a deeper nesting level", () => {
L176: const code = `
L177: ({
L178: safeProp: 1,
L179: nested: {
L180: allowed: 2,
L181: deeper: {
L182: constructor: "not allowed here"
L183: }
L184: }
L185: })
L186: `;
L187: const objExpr = parseObjectExpression(code);
L188: const result = validateObjectExpression(objExpr, 0, config);
L189: expect(result.isValid).toBe(false);
L190: expect(
L191: result.issues.some((issue) => issue.message.includes("is not allowed"))
L192: ).toBe(true);
L193: });
L194:
L195: it("should reject objects with spread elements", () => {
L196: const code = `
L197: ({
L198: ...spreadData
L199: })
L200: `;
L201: const objExpr = parseObjectExpression(code);
L202: const result = validateObjectExpression(objExpr, 0, config);
L203: expect(result.isValid).toBe(false);
L204: expect(result.issues[0].message).toContain(
L205: "Spread elements are not allowed"
L206: );
L207: });
L208: });
L209:
L210: // -----------------------
L211: // Schema Validator Tests
L212: // -----------------------
L213: describe("SchemaValidator - complex scenarios", () => {
L214: it("should handle multiple schema declarations and fail if one is invalid", async () => {
L215: const code = `
L216: import { z } from 'zod';
L217: const validSchema = z.string();
L218: const invalidSchema = undefined;
L219: export const anotherSchema = z.number();
L220: `;
L221: const result = await schemaValidator.validateSchema(code);
L222: expect(result.isValid).toBe(false);
L223: expect(
L224: result.issues.some((issue) =>
L225: issue.message.includes("Schema declaration must have an initializer")
L226: )
L227: ).toBe(true);
L228: });
L229:
L230: it("should fail if z is not imported from zod", async () => {
L231: const code = `
L232: import { z as differentName } from 'zod';
L233: export const testSchema = differentName.string();
L234: `;
L235: const result = await schemaValidator.validateSchema(code);
L236: expect(result.isValid).toBe(false);
L237: expect(
L238: result.issues.some((issue) =>
L239: issue.message.includes("Missing 'z' import from 'zod'")
L240: )
L241: ).toBe(true);
L242: });
L243:
L244: it("should fail if something else is imported from another library", async () => {
L245: const code = `
L246: import { z } from 'zod';
L247: import x from 'not-zod';
L248: const testSchema = z.string();
L249: `;
L250: const result = await schemaValidator.validateSchema(code);
L251: expect(result.isValid).toBe(false);
L252: expect(
L253: result.issues.some((issue) =>
L254: issue.message.includes("Only 'zod' imports are allowed")
L255: )
L256: ).toBe(true);
L257: });
L258: });
L259: });
L260:
</tests/complex.test.ts>
<tests/object-validator.test.ts>
L1: import { validateObjectExpression } from "../src/object-validator";
L2: import { createTestConfig } from "./test-utils";
L3: import { parse } from "@babel/parser";
L4: import { ObjectExpression, ExpressionStatement } from "@babel/types";
L5: import { IssueSeverity } from "../src/reporting";
L6:
L7: describe("ObjectValidator", () => {
L8: const config = createTestConfig();
L9:
L10: function parseObjectExpression(code: string): ObjectExpression {
L11: const ast = parse(code);
L12: const stmt = ast.program.body[0];
L13: if (!stmt || stmt.type !== "ExpressionStatement") {
L14: throw new Error("Expected expression statement");
L15: }
L16: const expr = (stmt as ExpressionStatement).expression;
L17: if (expr.type !== "ObjectExpression") {
L18: throw new Error("Expected object expression");
L19: }
L20: return expr;
L21: }
L22:
L23: it("should validate safe object expressions", () => {
L24: const code = `({
L25: name: "test",
L26: age: 42,
L27: tags: ["a", "b"]
L28: })`;
L29: const objExpr = parseObjectExpression(code);
L30: const result = validateObjectExpression(objExpr, 0, config);
L31: expect(result.isValid).toBe(true);
L32: expect(result.issues).toHaveLength(0);
L33: });
L34:
L35: it("should reject computed properties when not allowed", () => {
L36: const code = `({
L37: ["computed"]: "value"
L38: })`;
L39: const objExpr = parseObjectExpression(code);
L40: const result = validateObjectExpression(objExpr, 0, config);
L41: expect(result.isValid).toBe(false);
L42: expect(result.issues[0].message).toContain(
L43: "Computed properties are not allowed"
L44: );
L45: });
L46:
L47: it("should reject too many properties", () => {
L48: const customConfig = createTestConfig({ maxPropertiesPerObject: 1 });
L49: const code = `({ a: 1, b: 2 })`;
L50: const objExpr = parseObjectExpression(code);
L51: const result = validateObjectExpression(objExpr, 0, customConfig);
L52: expect(result.isValid).toBe(false);
L53: expect(result.issues[0].message).toContain(
L54: "Object exceeds maximum property count of 1"
L55: );
L56: });
L57:
L58: it("should reject object exceeding max depth", () => {
L59: const customConfig = createTestConfig({ maxObjectDepth: 1 });
L60: const code = `({
L61: nested: {
L62: another: "value"
L63: }
L64: })`;
L65: const objExpr = parseObjectExpression(code);
L66: const result = validateObjectExpression(objExpr, 0, customConfig);
L67: expect(result.isValid).toBe(false);
L68: expect(result.issues[0].message).toContain(
L69: "Object exceeds maximum nesting depth"
L70: );
L71: });
L72:
L73: it("should reject unsafe property names (deniedProperties)", () => {
L74: const code = `({
L75: constructor: "test"
L76: })`;
L77: const objExpr = parseObjectExpression(code);
L78: const result = validateObjectExpression(objExpr, 0, config);
L79: expect(result.isValid).toBe(false);
L80: expect(result.issues[0].message).toContain(
L81: "Property name 'constructor' is not allowed"
L82: );
L83: expect(result.issues[0].severity).toBe(IssueSeverity.WARNING);
L84: });
L85:
L86: it("should reject property name starting with a denied prefix", () => {
L87: const customConfig = createTestConfig({
L88: propertySafety: {
L89: ...config.propertySafety,
L90: deniedPrefixes: ["_"],
L91: },
L92: });
L93: const code = `({
L94: _secret: "data"
L95: })`;
L96: const objExpr = parseObjectExpression(code);
L97: const result = validateObjectExpression(objExpr, 0, customConfig);
L98: expect(result.isValid).toBe(false);
L99: expect(result.issues[0].message).toContain(
L100: "Property name '_secret' uses a forbidden prefix"
L101: );
L102: });
L103: });
L104:
</tests/object-validator.test.ts>
<tests/reporting.test.ts>
L1: import { IssueReporter, IssueSeverity } from "../src/reporting";
L2: import { Node } from "@babel/types";
L3:
L4: describe("IssueReporter", () => {
L5: let reporter: IssueReporter;
L6:
L7: beforeEach(() => {
L8: reporter = new IssueReporter();
L9: });
L10:
L11: it("should report issues correctly", () => {
L12: const dummyNode: Node = {
L13: type: "Identifier",
L14: loc: { start: { line: 5, column: 10 } },
L15: } as any;
L16: reporter.reportIssue(
L17: dummyNode,
L18: "Test message",
L19: "Identifier",
L20: IssueSeverity.WARNING,
L21: "Try something else"
L22: );
L23: const issues = reporter.getIssues();
L24: expect(issues).toHaveLength(1);
L25: expect(issues[0].message).toBe("Test message");
L26: expect(issues[0].line).toBe(5);
L27: expect(issues[0].column).toBe(10);
L28: expect(issues[0].severity).toBe(IssueSeverity.WARNING);
L29: expect(issues[0].suggestion).toBe("Try something else");
L30: });
L31:
L32: it("should get formatted report", () => {
L33: const dummyNode: Node = {
L34: type: "CallExpression",
L35: loc: { start: { line: 1, column: 1 } },
L36: } as any;
L37: reporter.reportIssue(
L38: dummyNode,
L39: "Another message",
L40: "CallExpression",
L41: IssueSeverity.ERROR
L42: );
L43: const report = reporter.getFormattedReport();
L44: expect(report).toContain("ERROR: Another message (CallExpression) at 1:1");
L45: });
L46:
L47: it("should filter issues by severity", () => {
L48: const node: Node = {
L49: type: "Literal",
L50: loc: { start: { line: 2, column: 3 } },
L51: } as any;
L52: reporter.reportIssue(node, "Error issue", "Literal", IssueSeverity.ERROR);
L53: reporter.reportIssue(
L54: node,
L55: "Warning issue",
L56: "Literal",
L57: IssueSeverity.WARNING
L58: );
L59:
L60: const errors = reporter.getIssuesBySeverity(IssueSeverity.ERROR);
L61: const warnings = reporter.getIssuesBySeverity(IssueSeverity.WARNING);
L62:
L63: expect(errors).toHaveLength(1);
L64: expect(warnings).toHaveLength(1);
L65: });
L66:
L67: it("should detect errors with hasErrors", () => {
L68: const node: Node = {
L69: type: "Literal",
L70: loc: { start: { line: 2, column: 2 } },
L71: } as any;
L72: expect(reporter.hasErrors()).toBe(false);
L73: reporter.reportIssue(node, "Some error", "Literal", IssueSeverity.ERROR);
L74: expect(reporter.hasErrors()).toBe(true);
L75: });
L76:
L77: it("should clear issues", () => {
L78: const node: Node = {
L79: type: "Literal",
L80: loc: { start: { line: 3, column: 4 } },
L81: } as any;
L82: reporter.reportIssue(node, "Some issue", "Literal", IssueSeverity.ERROR);
L83: expect(reporter.getIssues()).toHaveLength(1);
L84: reporter.clear();
L85: expect(reporter.getIssues()).toHaveLength(0);
L86: });
L87: });
L88:
</tests/reporting.test.ts>
<tests/resource-manager.test.ts>
L1: import { ResourceManager, ValidationError } from "../src/resource-manager";
L2: import { createTestConfig } from "./test-utils";
L3:
L4: describe("ResourceManager", () => {
L5: let manager: ResourceManager;
L6:
L7: beforeEach(() => {
L8: const config = createTestConfig({ timeoutMs: 100 });
L9: manager = new ResourceManager(config);
L10: });
L11:
L12: it("should increment node count without exceeding", () => {
L13: for (let i = 0; i < 10; i++) {
L14: manager.incrementNodeCount();
L15: }
L16: const stats = manager.getStats();
L17: expect(stats.nodeCount).toBe(10);
L18: });
L19:
L20: it("should throw ValidationError when node count exceeded", () => {
L21: const config = createTestConfig({ maxNodeCount: 5 });
L22: manager = new ResourceManager(config);
L23:
L24: expect(() => {
L25: for (let i = 0; i < 6; i++) {
L26: manager.incrementNodeCount();
L27: }
L28: }).toThrowError(ValidationError);
L29: });
L30:
L31: it("should throw ValidationError when timeout is exceeded", () => {
L32: const config = createTestConfig({ timeoutMs: 1 });
L33: manager = new ResourceManager(config);
L34:
L35: const originalDateNow = Date.now;
L36: jest.spyOn(Date, "now").mockImplementation(() => originalDateNow() + 2000);
L37:
L38: expect(() => manager.checkTimeout()).toThrowError(ValidationError);
L39: });
L40:
L41: it("should track depth and throw if exceeded", () => {
L42: const config = createTestConfig({ maxObjectDepth: 1 });
L43: manager = new ResourceManager(config);
L44:
L45: // Depth allowed is 1, let's do depth = 2
L46: expect(() => manager.trackDepth(2, "object")).toThrowError(ValidationError);
L47: });
L48:
L49: it("should validate size and throw if exceeded", () => {
L50: expect(() => manager.validateSize(101, 100, "array")).toThrowError(
L51: ValidationError
L52: );
L53: });
L54:
L55: it("should return stats", () => {
L56: manager.incrementNodeCount();
L57: const stats = manager.getStats();
L58: expect(stats.nodeCount).toBeGreaterThan(0);
L59: expect(stats.executionTime).toBeGreaterThanOrEqual(0);
L60: });
L61: });
L62:
</tests/resource-manager.test.ts>
<tests/schema-validator.test.ts>
L1: import { SchemaValidator } from "../src/schema-validator";
L2: import {
L3: TestResourceManager,
L4: createTestConfig,
L5: createSchemaInput,
L6: expectValidationIssues,
L7: testSchemas,
L8: } from "./test-utils";
L9: import { IssueReporter } from "../src/reporting";
L10:
L11: describe("SchemaValidator", () => {
L12: let resourceManager: TestResourceManager;
L13: let issueReporter: IssueReporter;
L14: let validator: SchemaValidator;
L15:
L16: beforeEach(() => {
L17: const config = createTestConfig();
L18: resourceManager = new TestResourceManager(config);
L19: issueReporter = new IssueReporter();
L20: validator = new SchemaValidator(config, resourceManager, issueReporter);
L21: });
L22:
L23: it("should validate a simple schema", async () => {
L24: const result = await validator.validateSchema(
L25: createSchemaInput(testSchemas.basic)
L26: );
L27: expect(result.isValid).toBe(true);
L28: expect(result.issues).toHaveLength(0);
L29: });
L30:
L31: it("should handle schemas missing z import", async () => {
L32: const code = `export const testSchema = z.object({});`;
L33: // Missing import statement
L34: const result = await validator.validateSchema(code);
L35: expect(result.isValid).toBe(false);
L36: expect(result.issues[0].message).toContain("Missing 'z' import from 'zod'");
L37: });
L38:
L39: it("should remove invalid imports and report issues", async () => {
L40: const code = `
L41: import { something } from 'somewhere';
L42: import { z } from 'zod';
L43: export const testSchema = z.string();
L44: `;
L45: const result = await validator.validateSchema(code);
L46: expect(result.isValid).toBe(false);
L47: expect(result.issues[0].message).toContain(
L48: "Only 'zod' imports are allowed"
L49: );
L50: });
L51:
L52: it("should reject variable declarations not using const", async () => {
L53: const code = `
L54: import { z } from 'zod';
L55: var testSchema = z.string();
L56: `;
L57: const result = await validator.validateSchema(code);
L58: expect(result.isValid).toBe(false);
L59: expect(result.issues[0].message).toContain(
L60: "Schema declarations must use 'const'"
L61: );
L62: });
L63:
L64: it("should reject schema declaration without initializer", async () => {
L65: const code = `
L66: import { z } from 'zod';
L67: const testSchema = undefined;
L68: `;
L69: const result = await validator.validateSchema(code);
L70: expect(result.isValid).toBe(false);
L71: expect(result.issues[0].message).toContain(
L72: "Schema declaration must have an initializer"
L73: );
L74: });
L75:
L76: it("should reject invalid statements", async () => {
L77: const code = `
L78: import { z } from 'zod';
L79: function notAllowed() {}
L80: const testSchema = z.string();
L81: `;
L82: const result = await validator.validateSchema(code);
L83: expect(result.isValid).toBe(false);
L84: expect(
L85: result.issues.some((issue) =>
L86: issue.message.includes("Invalid statement type")
L87: )
L88: ).toBe(true);
L89: });
L90:
L91: it("should fail when node count exceeds limit", async () => {
L92: resourceManager.setMockNodeCount(1000);
L93: const result = await validator.validateSchema(
L94: createSchemaInput(testSchemas.complex)
L95: );
L96: expect(result.isValid).toBe(false);
L97: expectValidationIssues(result.issues, [{ message: "Node count exceeded" }]);
L98: });
L99:
L100: it("should fail on timeout", async () => {
L101: resourceManager.triggerTimeout();
L102: const result = await validator.validateSchema(
L103: createSchemaInput(testSchemas.basic)
L104: );
L105: expect(result.isValid).toBe(false);
L106: expectValidationIssues(result.issues, [{ message: "Timeout triggered" }]);
L107: });
L108:
L109: it("should generate cleaned code if valid", async () => {
L110: const code = `
L111: import { z } from 'zod';
L112: const userSchema = z.object({ name: z.string() });
L113: `;
L114: const result = await validator.validateSchema(code);
L115: expect(result.isValid).toBe(true);
L116: expect(result.cleanedCode).toContain("z.object({");
L117: expect(result.cleanedCode).toContain("name: z.string()");
L118: });
L119: });
L120:
</tests/schema-validator.test.ts>
<tests/test-utils.ts>
L1: import { ValidationConfig, createConfig, relaxedConfig } from "../src/types";
L2: import { ResourceManager } from "../src/resource-manager";
L3: import { parse } from "@babel/parser";
L4: import { CallExpression } from "@babel/types";
L5:
L6: /**
L7: * Creates a mock resource manager for testing
L8: * Allows controlling timeouts, node counts etc.
L9: */
L10: export class TestResourceManager extends ResourceManager {
L11: private mockNodeCount = 0;
L12: private mockTimeoutTriggered = false;
L13:
L14: constructor(config: ValidationConfig) {
L15: super(config);
L16: }
L17:
L18: public setMockNodeCount(count: number) {
L19: this.mockNodeCount = count;
L20: }
L21:
L22: public triggerTimeout() {
L23: this.mockTimeoutTriggered = true;
L24: }
L25:
L26: public override incrementNodeCount(): void {
L27: if (this.mockTimeoutTriggered) {
L28: throw new Error("Timeout triggered");
L29: }
L30: if (this.mockNodeCount >= this.config.maxNodeCount) {
L31: throw new Error("Node count exceeded");
L32: }
L33: this.mockNodeCount++;
L34: }
L35: }
L36:
L37: /**
L38: * Helper to parse code into a CallExpression AST node
L39: */
L40: export function parseCallExpression(code: string): CallExpression {
L41: const ast = parse(code);
L42: const stmt = ast.program.body[0];
L43: if (stmt.type !== "ExpressionStatement") {
L44: throw new Error("Expected expression statement");
L45: }
L46: const expr = stmt.expression;
L47: if (expr.type !== "CallExpression") {
L48: throw new Error("Expected call expression");
L49: }
L50: return expr;
L51: }
L52:
L53: /**
L54: * Creates a test configuration with specific overrides
L55: */
L56: export function createTestConfig(
L57: overrides?: Partial<ValidationConfig>
L58: ): ValidationConfig {
L59: return createConfig(relaxedConfig, {
L60: timeoutMs: 1000,
L61: maxNodeCount: 100,
L62: allowComputedProperties: false, // Explicitly disable computed properties for tests
L63: ...overrides,
L64: });
L65: }
L66:
L67: /**
L68: * Helper to create schema validation inputs
L69: */
L70: export function createSchemaInput(schema: string): string {
L71: // Ensure proper formatting and no extra whitespace
L72: return `import { z } from 'zod';\\\\nexport const testSchema = ${schema};`;
L73: }
L74:
L75: /**
L76: * Verifies that validation produces expected issues
L77: */
L78: export function expectValidationIssues(
L79: issues: Array<{ message: string; nodeType: string }>,
L80: expectedIssues: Array<Partial<{ message: string; nodeType: string }>>
L81: ) {
L82: // Check that all expected issues are present, in any order
L83: expectedIssues.forEach((expected) => {
L84: const matchingIssue = issues.find((issue) => {
L85: if (expected.message && !issue.message.includes(expected.message)) {
L86: return false;
L87: }
L88: if (expected.nodeType && issue.nodeType !== expected.nodeType) {
L89: return false;
L90: }
L91: return true;
L92: });
L93:
L94: if (!matchingIssue) {
L95: throw new Error(
L96: `Expected to find issue matching ${JSON.stringify(expected)}\\\\n` +
L97: `Actual issues: ${JSON.stringify(issues, null, 2)}`
L98: );
L99: }
L100: });
L101: }
L102:
L103: /**
L104: * Common test schemas
L105: */
L106: export const testSchemas = {
L107: basic: `z.object({ name: z.string() })`,
L108: unsafe: `z.object({ constructor: z.function() })`,
L109: complex: `
L110: z.object({
L111: id: z.string().uuid(),
L112: data: z.record(z.string(), z.any()),
L113: meta: z.object({
L114: created: z.date(),
L115: tags: z.array(z.string())
L116: })
L117: })
L118: `,
L119: };
L120:
</tests/test-utils.ts>
<tests/data/testSchema1.ts>
L1: import { z } from "zod";
L2:
L3: let a = 12;
L4:
L5: const userProfileSchema = z.object({
L6: avatar_hash: z.string(),
L7: image_72: z.string().url(),
L8: first_name: z.string(),
L9: real_name: z.string(),
L10: display_name: z.string(),
L11: team: z.string(),
L12: name: z.string(),
L13: is_restricted: z.boolean(),
L14: is_ultra_restricted: z.boolean(),
L15: });
L16:
L17: callFunc();
L18:
L19: const richTextSectionElementSchema = z.object({
L20: type: z.literal("text").or(z.literal("link")),
L21: text: z.string().optional(),
L22: url: z.string().url().optional(),
L23: });
L24:
L25: const richTextSectionSchema = z.object({
L26: type: z.literal("rich_text_section"),
L27: elements: z.array(richTextSectionElementSchema),
L28: });
L29:
L30: const richTextListSchema = z.object({
L31: type: z.literal("rich_text_list"),
L32: elements: z.array(richTextSectionSchema),
L33: style: z.literal("bullet"),
L34: indent: z.number(),
L35: border: z.number(),
L36: });
L37:
L38: const richTextElementSchema = z.object({
L39: type: z.literal("rich_text"),
L40: block_id: z.string(),
L41: elements: z.array(richTextSectionSchema.or(richTextListSchema)),
L42: });
L43:
L44: const videoBlockSchema = z.object({
L45: type: z.literal("video"),
L46: block_id: z.string(),
L47: video_url: z.string().url(),
L48: thumbnail_url: z.string().url(),
L49: alt_text: z.string(),
L50: title: z.object({
L51: type: z.literal("plain_text"),
L52: text: z.string(),
L53: emoji: z.boolean(),
L54: }),
L55: title_url: z.string().url(),
L56: author_name: z.string(),
L57: provider_name: z.string(),
L58: provider_icon_url: z.string().url(),
L59: });
L60:
L61: const sectionBlockSchema = z.object({
L62: type: z.literal("section"),
L63: block_id: z.string(),
L64: text: z.object({
L65: type: z.literal("plain_text"),
L66: text: z.string(),
L67: emoji: z.boolean(),
L68: }),
L69: });
L70:
L71: const attachmentSchema = z.object({
L72: id: z.number(),
L73: blocks: z.array(videoBlockSchema.or(sectionBlockSchema)),
L74: fallback: z.string(),
L75: bot_id: z.string(),
L76: app_unfurl_url: z.string().url(),
L77: is_app_unfurl: z.boolean(),
L78: app_id: z.string(),
L79: });
L80:
L81: const messageSchema = z.object({
L82: user: z.string(),
L83: type: z.literal("message"),
L84: ts: z.string(),
L85: client_msg_id: z.string(),
L86: text: z.string(),
L87: team: z.string(),
L88: user_team: z.string(),
L89: source_team: z.string(),
L90: user_profile: userProfileSchema,
L91: attachments: z.array(attachmentSchema).optional(),
L92: blocks: z.array(richTextElementSchema),
L93: edited: z
L94: .object({
L95: user: z.string(),
L96: ts: z.string(),
L97: })
L98: .optional(),
L99: thread_ts: z.string().optional(),
L100: parent_user_id: z.string().optional(),
L101: });
L102:
L103: export default messageSchema;
L104:
</tests/data/testSchema1.ts>
<package.json>
L1: {
L2: "name": "zodsheriff",
L3: "version": "0.0.1",
L4: "author": "Hrishi Olickel <twitter-@hrishioa> (<https://olickel.com>)",
L5: "repository": {
L6: "type": "git",
L7: "url": "git+https://github.com/southbridgeai/zodsheriff.git"
L8: },
L9: "main": "./dist/index.cjs",
L10: "bin": {
L11: "zodsheriff": "./dist/run.js"
L12: },
L13: "module": "./dist/index.js",
L14: "devDependencies": {
L15: "@biomejs/biome": "^1.9.4",
L16: "@swc/core": "^1.7.26",
L17: "@types/bun": "^1.1.10",
L18: "@types/jest": "^29.5.14",
L19: "@types/node": "^22.7.4",
L20: "jest": "^29.7.0",
L21: "ts-jest": "^29.2.5",
L22: "tsup": "^8.3.0",
L23: "typescript": "^5.6.2"
L24: },
L25: "exports": {
L26: ".": {
L27: "import": {
L28: "types": "./dist/index.d.mts",
L29: "default": "./dist/index.js"
L30: },
L31: "require": {
L32: "types": "./dist/index.d.cts",
L33: "default": "./dist/index.cjs"
L34: }
L35: }
L36: },
L37: "description": "Validation for LLM-generated typescript zod schemas",
L38: "files": [
L39: "dist",
L40: "package.json"
L41: ],
L42: "license": "CC By-NC 4.0",
L43: "scripts": {
L44: "build": "tsup src/index.ts src/run.ts && tsc --emitDeclarationOnly --declaration --declarationDir dist && mv dist/index.d.ts dist/index.d.mts && cp dist/index.d.mts dist/index.d.cts",
L45: "test": "jest",
L46: "cli": "node dist/run.js"
L47: },
L48: "type": "module",
L49: "types": "./dist/index.d.cts",
L50: "dependencies": {
L51: "@babel/generator": "^7.26.3",
L52: "@babel/parser": "^7.26.3",
L53: "@babel/traverse": "^7.26.4",
L54: "clipboardy": "^4.0.0",
L55: "safe-regex": "^2.1.1"
L56: }
L57: }
L58:
</package.json>
<h1 align="center">
<br>
<a href="<https://github.com/hrishioa/lumentis>"><img src="<https://github.com/hrishioa/lumentis/assets/973967/73832318-5e90-4191-bbbb-324524ff4468>" alt="Lumentis" width="200"></a>
<br>
<code>npx lumentis</code>
<br>
</h1>
<h3 align="center">Generate beautiful docs from your transcripts and unstructured information with a single command.</h3>
<div align="center">
<a href="<https://trendshift.io/repositories/8853>" target="_blank"><img src="<https://trendshift.io/api/badge/repositories/8853>" alt="hrishioa%2Flumentis | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
A simple way to generate comprehensive, easy-to-skim docs from your meeting transcripts and large documents. Now supports GPT-4 Omni and Gemini Flash!
[![Twitter Follow](<https://img.shields.io/twitter/follow/hrishi?style=social>)](<https://twitter.com/hrishioa>)
</div>
<div align="center">
![lumentis](<https://github.com/hrishioa/lumentis/assets/973967/cd16bc41-bd8a-40b6-97b0-c3b57d4650cb>)
</div>
## How to use
1. Run `npx lumentis` in an empty directory. That's really it. You can skip the rest of this README.
(Known issue if you've run Lumentis before: clear your npx cache with `npx clear-npx-cache` or you might get link errors. If you don't want to, you can also run `npx [email protected]`.)
(DON'T run lumentis in the cloned repo!)
2. Feed it a transcript, doc or notes when asked.
3. Answer some questions about themes and audience.
4. Pick what you like from the generated outline.
5. Wait for your docs to be written up!
6. [Deploy your docs to Vercel](<https://vercel.com/docs/deployments/overview>) by pushing your folder and following the guide.
## Examples
Lumentis lets you swap models between stages. Here's some docs exactly as Lumentis generated them, no editing. I just hit Enter a few times.
1. **[The Feynman Lectures on Physics](<https://feynman-lectures.vercel.app/>)** - taken from the [5 hour Feynman Lectures](<https://www.youtube.com/watch?v=kEx-gRfuhhk>), this is Sonnet doing the hard work for 72 cents, and Haiku writing it out for 38 cents.
2. **[Designing Frictionless Interfaces for Google](<https://designing-better-ui.vercel.app/>)** - Mustafa Kurtuldu gave a wonderful talk on design and UX I wish more people would watch. Now you can read it. [(Do still watch it)](<https://www.youtube.com/watch?v=Drf5ZKd4aVY>) but this is Haiku doing the whole thing for less than 8 (not eighty) cents!
3. **[How the AI in Spiderman 2 works](<https://spiderman-2-ai-mechanics.vercel.app/>)** - from [something that's been on my list](<https://www.youtube.com/watch?v=LxWq65CZBU8>) for a long time. Opus took about $3.80 to do the whole thing.
4. **[Sam Altman and Lex Friedman on GPT-5](<https://sam-lex-gpt5.vercel.app/>)** - Sam and Lex [had a conversation](<https://www.youtube.com/watch?v=jvqFAi7vkBc>) recently. Here's Opus doing the hard work for $2.3, and Sonnet doing the rest for $2.5. This is the expensive option.
5. **[Self-Discover in DSPy with Chris Dossman](<https://lumentis-autogen-dspy-weviate-podcast.vercel.app/>)** - [an interesting conversation between Chris Dossman and Weviate](<https://www.youtube.com/watch?v=iC64q1gFWiY>) about DSPy and structured reasoning, one of the core concepts behind the framework. [Eugene](<https://github.com/eugene-yaroslavtsev>) splurged something like $25 on this 😱 because he wanted to see how Lumentis would do at its best.
6. **[John Shulman OpenAI Podcast with GPT-4o](<https://john-shulman-gpt4o-gpt4o.vercel.app/>)** - generated for about $1 in less than 20 seconds with GPT-4 Omni, from [this awesome podcast](<https://www.youtube.com/watch?v=Wo95ob_s_NI>)!
7. **[John Shulman Podcast with GPT-4o and Gemini Flash](<https://john-shulman-gpt4o-gemini-flash.vercel.app/>)** - generated for about the same in less than 10 seconds with GPT-4 Omni and Gemini Flash.
## Features
- Cost before run: Lumentis will dynamically tell you what each operation costs.
- Switch models: Use a smarter model to do the hard parts, and a cheaper model for long-form work. See the examples.
- Easy to change: Ctrl+C at any time and restart. Lumentis remembers your responses, and lets you change them.
- Everything in the open: want to know how it works? Check the `.lumentis` folder to see every message and response to the AI.
- Super clean: Other than `.lumentis` with the prompts and state, you have a clean project to do anything with. Git/Vercel/Camera ready.
- Super fast: (If you run with `bun`. Can't vouch for npm.)
## How it works
Lumentis reads your transcript and:
1. Asks you some questions to understand the themes and audience. Also to surf the latent space or things.
2. Generates an outline and asks you to select what you want to keep.
3. Auto generates structure from the information and further refines it with your input, while self-healing things.
4. Generates detailed pages with visual variety, formatting and styles.
## Coming soon (when I have a free night)
1. Folders
2. PDFs
3. Auto-transcription with a rubber ducky
4. Scraping entire websites
5. Scientific papers
6. Recursive summarisation and expansion
7. Continuously updating docs
## Development
```bash
git clone <https://github.com/hrishioa/lumentis.git>
cd lumentis
bun install
bun run run
Using bun because it's fast. You can also use npm or yarn if you prefer.
Try it out and let me know the URL so I can add it here! There's also some badly organized things in TODO.md
that I need to get around to.
this is the code for a package I developed. Help me write a really good README for this. I've also provided a good example of a README, albeit for a much much simpler project.
Here are some guidelines. READMEs should make it easy for someone to try the project, to understand where it can be useful, to seem cool, and to get the beginnings of how it can be used. For more complex projects (like this one), you also want to mention design decisions and tradeoffs, to hold off early questions that you expect will be asked.
Here, We want to highlight that the key use-case is being able to safely execute schemas generated by LLMs. we should also add a section to cover the decisions made in how this is architected - where we draw the line between safety and convenience, what the choices are, etc. and also highlight future possible work and ways to improve. Let's remove the license and contributing section. Let's also make the key features have descriptions that clearly communicate what the big benefits are from a user-centric perspective.
With those in mind, can you write it in good github markdown? can you write me a new large section on the design considerations in actual specifics? Like how we ignore bad paths, keep comments in, turn all schemas into exported consts, etc? Work some of those into the beginning as well. We don't have a logo yet, so feel free to use emojis. Feel free to be verbose in later sections.