Skip to content

Parse nullable args #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/parse/__tests__/ContractAnalyzer.multiFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,4 +1188,92 @@ describe('ContractAnalyzer - Multi-File Analysis', () => {
]);
});
});

describe('null and undefined union types', () => {
it('should handle multi-file contract with null union parameters', () => {
const sourceFiles = {
'src/types.ts': `
export interface UserData {
id: string;
name: string | null;
}
`,
'src/contract.ts': `
import { UserData } from './types';

export default class NullableContract {
state = { users: [] as UserData[] };

addUser(userData: UserData | null): boolean {
if (userData) {
this.state.users.push(userData);
return true;
}
return false;
}

findUser(id: string | null): UserData | null {
return id ? this.state.users.find(u => u.id === id) || null : null;
}
}
`,
};

const result = analyzer.analyzeMultiFile(sourceFiles);
expect(result.queries).toEqual([
{
name: 'findUser',
params: [{ name: 'id', type: 'string | null' }],
returnType: 'UserData | null',
},
]);
expect(result.mutations).toEqual([
{
name: 'addUser',
params: [{ name: 'userData', type: 'UserData | null' }],
returnType: 'boolean',
},
]);
});

it('should handle multi-file contract with undefined union parameters', () => {
const sourceFiles = {
'src/models.ts': `
export type OptionalString = string | undefined;
export type NullableNumber = number | null | undefined;
`,
'src/contract.ts': `
import { OptionalString, NullableNumber } from './models';

export default class UndefinedContract {
state = { data: {} };

processValue(value: OptionalString): string {
return value || 'default';
}

updateCounter(increment: NullableNumber): void {
this.state.data.counter = (this.state.data.counter || 0) + (increment || 0);
}
}
`,
};

const result = analyzer.analyzeMultiFile(sourceFiles);
expect(result.queries).toEqual([
{
name: 'processValue',
params: [{ name: 'value', type: 'OptionalString' }],
returnType: 'string',
},
]);
expect(result.mutations).toEqual([
{
name: 'updateCounter',
params: [{ name: 'increment', type: 'NullableNumber' }],
returnType: 'void',
},
]);
});
});
});
154 changes: 152 additions & 2 deletions packages/parse/__tests__/ContractAnalyzer.multiFileSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
{
name: 'checkStatus',
params: [],
returnSchema: { anyOf: [{ type: 'boolean' }, {}] },
returnSchema: { anyOf: [{ type: 'boolean' }, { type: 'null' }] },
},
]);
expect(result.mutations).toEqual([
Expand Down Expand Up @@ -587,7 +587,7 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
},
required: ['id', 'name', 'age'],
},
{},
{ type: 'null' },
],
},
},
Expand Down Expand Up @@ -1076,4 +1076,154 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
},
]);
});

describe('null and undefined union types with schemas', () => {
it('should handle multi-file contract with null union schemas', () => {
const sourceFiles = {
'src/types.ts': `
export interface UserProfile {
id: string;
email: string | null;
verified: boolean;
}
`,
'src/contract.ts': `
import { UserProfile } from './types';

export default class NullableSchemaContract {
state = { profiles: [] as UserProfile[] };

getProfile(id: string | null): UserProfile | null {
return this.state.profiles[0];
}

createProfile(data: UserProfile | null): void {
if (data) {
this.state.profiles.push(data);
}
}
}
`,
};

const result = analyzer.analyzeMultiFileWithSchema(sourceFiles);
expect(result.queries).toEqual([
{
name: 'getProfile',
params: [
{
name: 'id',
schema: { anyOf: [{ type: 'string' }, { type: 'null' }] },
},
],
returnSchema: {
anyOf: [
{
type: 'object',
properties: {
id: { type: 'string' },
email: { anyOf: [{ type: 'string' }, { type: 'null' }] },
verified: { type: 'boolean' },
},
required: ['id', 'email', 'verified'],
},
{ type: 'null' },
],
},
},
]);
expect(result.mutations).toEqual([
{
name: 'createProfile',
params: [
{
name: 'data',
schema: {
anyOf: [
{
type: 'object',
properties: {
id: { type: 'string' },
email: { anyOf: [{ type: 'string' }, { type: 'null' }] },
verified: { type: 'boolean' },
},
required: ['id', 'email', 'verified'],
},
{ type: 'null' },
],
},
},
],
returnSchema: {},
},
]);
});

it('should handle multi-file contract with undefined union schemas', () => {
const sourceFiles = {
'src/config.ts': `
export type OptionalConfig = {
timeout?: number | undefined;
retries: number | null;
};
`,
'src/contract.ts': `
import { OptionalConfig } from './config';

export default class UndefinedSchemaContract {
state = { settings: {} };

updateConfig(config: OptionalConfig): OptionalConfig {
this.state.settings = { ...this.state.settings, ...config };
return config;
}

getTimeout(defaultValue: number | undefined): number {
return this.state.settings.timeout ?? defaultValue ?? 5000;
}
}
`,
};

const result = analyzer.analyzeMultiFileWithSchema(sourceFiles);
expect(result.queries).toEqual([
{
name: 'getTimeout',
params: [
{
name: 'defaultValue',
schema: { anyOf: [{ type: 'number' }, { type: 'null' }] },
},
],
returnSchema: { type: 'number' },
},
]);
expect(result.mutations).toEqual([
{
name: 'updateConfig',
params: [
{
name: 'config',
schema: {
type: 'object',
properties: {
timeout: { anyOf: [{ type: 'number' }, { type: 'null' }] },
retries: { anyOf: [{ type: 'number' }, { type: 'null' }] },
},
required: ['retries'],
},
},
],
returnSchema: {
type: 'object',
properties: {
timeout: { anyOf: [{ type: 'number' }, { type: 'null' }] },
retries: { anyOf: [{ type: 'number' }, { type: 'null' }] },
},
required: ['retries'],
},
},
]);
});
});
});
68 changes: 68 additions & 0 deletions packages/parse/__tests__/ContractAnalyzer.singleFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,4 +666,72 @@ describe('ContractAnalyzer - Single File Analysis', () => {
expect(result.mutations.map((m) => m.name)).toEqual(['addItem', 'processItem']);
});
});

describe('null and undefined union types', () => {
it('should handle parameters with null union types', () => {
const code = `
export default class Contract {
state: any;

add(num: number | null) {
if (num !== null) {
this.state.value += num;
}
}

getName(id: string | null): string {
return id ? this.state.names[id] : 'unknown';
}
}
`;

const result = analyzer.analyzeFromCode(code);
expect(result.queries).toEqual([
{
name: 'getName',
params: [{ name: 'id', type: 'string | null' }],
returnType: 'string',
},
]);
expect(result.mutations).toEqual([
{
name: 'add',
params: [{ name: 'num', type: 'number | null' }],
returnType: 'void',
},
]);
});

it('should handle parameters with undefined union types and nullable return types', () => {
const code = `
export default class Contract {
state: any;

updateCount(increment: number | undefined) {
this.state.count += increment || 0;
}

findUser(id: string): User | null | undefined {
return this.state.users.find(u => u.id === id);
}
}
`;

const result = analyzer.analyzeFromCode(code);
expect(result.queries).toEqual([
{
name: 'findUser',
params: [{ name: 'id', type: 'string' }],
returnType: 'User | null | undefined',
},
]);
expect(result.mutations).toEqual([
{
name: 'updateCount',
params: [{ name: 'increment', type: 'number | undefined' }],
returnType: 'void',
},
]);
});
});
});
Loading