Skip to content

Commit c369836

Browse files
throw lint error on complex_data_type field for primitive type
1 parent 0e96010 commit c369836

2 files changed

Lines changed: 223 additions & 43 deletions

File tree

dialect/agentforce/src/lint/passes/complex-data-type.ts

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
*/
77

88
/**
9-
* Complex data type warning rule for Agentforce.
9+
* Complex data type rule for Agentforce.
1010
*
11-
* Warns when object-type action inputs/outputs lack schema information:
12-
* - Inputs: should have complex_data_type_name or schema
13-
* - Outputs: should have complex_data_type_name
11+
* Only `object` and `list[object]` declarations support a `complex_data_type_name`.
1412
*
15-
* Diagnostic: object-type-missing-schema
13+
* - When a whitelisted type (`object` / `list[object]`) lacks schema info:
14+
* - Inputs: should have `complex_data_type_name` or `schema` (warning)
15+
* - Outputs: should have `complex_data_type_name` (warning)
16+
* - When a non-whitelisted (primitive) type has `complex_data_type_name`: error.
17+
*
18+
* Diagnostics: object-type-missing-schema, complex-data-type-on-primitive
1619
*/
1720

1821
import type { AstNodeLike, AstRoot, NamedMap } from '@agentscript/language';
@@ -37,9 +40,14 @@ function getTypeText(decl: Record<string, unknown>): string | null {
3740
return cst?.node?.text?.trim() ?? null;
3841
}
3942

40-
/** Check if a type string represents an object type. */
41-
function isObjectType(typeText: string): boolean {
42-
return typeText === 'object' || typeText === 'list[object]';
43+
/**
44+
* These required complex data types creates a warning without `complex_data_type_name` field.
45+
* Anything outside this set is treated as a primitive and does not need a `complex_data_type_name`.
46+
*/
47+
const REQURIED_COMPLEX_DATA_TYPE = new Set<string>(['object', 'list[object]']);
48+
49+
function isComplexType(typeText: string): boolean {
50+
return REQURIED_COMPLEX_DATA_TYPE.has(typeText);
4351
}
4452

4553
/** Check if a field has a non-empty string value. */
@@ -86,60 +94,66 @@ class ComplexDataTypePass implements LintPass {
8694
if (!actBlock || typeof actBlock !== 'object') continue;
8795
const act = actBlock as Record<string, unknown>;
8896

89-
this.checkInputs(act.inputs, actionName);
90-
this.checkOutputs(act.outputs, actionName);
97+
this.checkDecls(act.inputs, actionName, 'input');
98+
this.checkDecls(act.outputs, actionName, 'output');
9199
}
92100
}
93101
}
94102
}
95103

96-
private checkInputs(inputs: unknown, actionName: string): void {
97-
if (!inputs || !isNamedMap(inputs)) return;
104+
private checkDecls(
105+
decls: unknown,
106+
actionName: string,
107+
kind: 'input' | 'output'
108+
): void {
109+
if (!decls || !isNamedMap(decls)) return;
98110

99-
for (const [paramName, decl] of inputs as NamedMap<unknown>) {
111+
for (const [paramName, decl] of decls as NamedMap<unknown>) {
100112
if (!decl || typeof decl !== 'object') continue;
101113
const obj = decl as AstNodeLike;
102114
const typeText = getTypeText(obj as Record<string, unknown>);
103-
if (!typeText || !isObjectType(typeText)) continue;
115+
if (!typeText) continue;
104116

105117
const props = (obj as Record<string, unknown>).properties as
106118
| Record<string, unknown>
107119
| undefined;
108-
if (
109-
!hasStringField(props, 'complex_data_type_name') &&
110-
!hasStringField(props, 'schema')
111-
) {
112-
attachDiagnostic(
113-
obj,
114-
lintDiagnostic(
115-
getDeclRange(obj),
116-
`Action input '${paramName}' in '${actionName}' has type '${typeText}' but lacks 'complex_data_type_name' or 'schema'. Consider specifying the object schema for better type validation.`,
117-
DiagnosticSeverity.Warning,
118-
'object-type-missing-schema'
119-
)
120-
);
120+
const hasComplexDataTypeField = hasStringField(
121+
props,
122+
'complex_data_type_name'
123+
);
124+
125+
if (!isComplexType(typeText)) {
126+
// Primitive types must NOT declare complex_data_type_name.
127+
if (hasComplexDataTypeField) {
128+
attachDiagnostic(
129+
obj,
130+
lintDiagnostic(
131+
getDeclRange(obj),
132+
`Action ${kind} '${paramName}' in '${actionName}' has primitive type '${typeText}' and must not specify 'complex_data_type_name'. Only 'object' and 'list[object]' types support 'complex_data_type_name'.`,
133+
DiagnosticSeverity.Error,
134+
'complex-data-type-on-primitive'
135+
)
136+
);
137+
}
138+
continue;
121139
}
122-
}
123-
}
124140

125-
private checkOutputs(outputs: unknown, actionName: string): void {
126-
if (!outputs || !isNamedMap(outputs)) return;
127-
128-
for (const [outputName, decl] of outputs as NamedMap<unknown>) {
129-
if (!decl || typeof decl !== 'object') continue;
130-
const obj = decl as AstNodeLike;
131-
const typeText = getTypeText(obj as Record<string, unknown>);
132-
if (!typeText || !isObjectType(typeText)) continue;
133-
134-
const props = (obj as Record<string, unknown>).properties as
135-
| Record<string, unknown>
136-
| undefined;
137-
if (!hasStringField(props, 'complex_data_type_name')) {
141+
// Complex types should declare schema info.
142+
// Inputs may use `schema` as an alternative to `complex_data_type_name`.
143+
const hasSchema =
144+
hasComplexDataTypeField ||
145+
(kind === 'input' && hasStringField(props, 'schema'));
146+
console.log('Schema: ', hasSchema);
147+
if (!hasSchema) {
148+
const required =
149+
kind === 'input'
150+
? `'complex_data_type_name' or 'schema'`
151+
: `'complex_data_type_name'`;
138152
attachDiagnostic(
139153
obj,
140154
lintDiagnostic(
141155
getDeclRange(obj),
142-
`Action output '${outputName}' in '${actionName}' has type '${typeText}' but lacks 'complex_data_type_name'. Consider specifying the object schema for better type validation.`,
156+
`Action ${kind} '${paramName}' in '${actionName}' has type '${typeText}' but lacks ${required}. Consider specifying the object schema for better type validation.`,
143157
DiagnosticSeverity.Warning,
144158
'object-type-missing-schema'
145159
)

dialect/agentforce/src/tests/lint.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,3 +2344,169 @@ subagent Order_Management:
23442344
);
23452345
expect(warnings.length).toBeGreaterThan(0);
23462346
});
2347+
2348+
describe('complex data type rule', () => {
2349+
const wrap = (inputs: string, outputs: string): string => `
2350+
subagent S:
2351+
description: "S"
2352+
actions:
2353+
A:
2354+
description: "A"
2355+
inputs:
2356+
${inputs}
2357+
outputs:
2358+
${outputs}
2359+
reasoning:
2360+
instructions: ->
2361+
|Do it
2362+
`;
2363+
2364+
it('errors when a primitive input has complex_data_type_name', () => {
2365+
const diagnostics = runSecurityLint(
2366+
wrap(
2367+
` amount: number\n complex_data_type_name: "lightning__objectType"\n`,
2368+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2369+
)
2370+
);
2371+
const errors = diagnostics.filter(
2372+
d => d.code === 'complex-data-type-on-primitive'
2373+
);
2374+
expect(errors).toHaveLength(1);
2375+
expect(errors[0].severity).toBe(DiagnosticSeverity.Error);
2376+
expect(errors[0].message).toContain("'amount'");
2377+
expect(errors[0].message).toContain("'A'");
2378+
expect(errors[0].message).toContain("'number'");
2379+
});
2380+
2381+
it('does not flag primitive inputs without complex_data_type_name', () => {
2382+
const diagnostics = runSecurityLint(
2383+
wrap(
2384+
` amount: number\n description: "an amount"\n`,
2385+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2386+
)
2387+
);
2388+
expect(
2389+
diagnostics.filter(d => d.code === 'complex-data-type-on-primitive')
2390+
).toHaveLength(0);
2391+
});
2392+
2393+
it('errors when a primitive output has complex_data_type_name', () => {
2394+
const diagnostics = runSecurityLint(
2395+
wrap(
2396+
` in_ok: object\n complex_data_type_name: "lightning__objectType"\n`,
2397+
` message: string\n complex_data_type_name: "lightning__objectType"\n`
2398+
)
2399+
);
2400+
const errors = diagnostics.filter(
2401+
d => d.code === 'complex-data-type-on-primitive'
2402+
);
2403+
expect(errors).toHaveLength(1);
2404+
expect(errors[0].message).toContain("'message'");
2405+
expect(errors[0].message).toContain("'string'");
2406+
});
2407+
2408+
it.each([
2409+
['boolean'],
2410+
['integer'],
2411+
['id'],
2412+
['date'],
2413+
['datetime'],
2414+
['time'],
2415+
['timestamp'],
2416+
['currency'],
2417+
['long'],
2418+
])('errors when primitive type %s has complex_data_type_name', primitive => {
2419+
const diagnostics = runSecurityLint(
2420+
wrap(
2421+
` v: ${primitive}\n complex_data_type_name: "lightning__objectType"\n`,
2422+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2423+
)
2424+
);
2425+
const errors = diagnostics.filter(
2426+
d => d.code === 'complex-data-type-on-primitive'
2427+
);
2428+
expect(errors).toHaveLength(1);
2429+
expect(errors[0].message).toContain(`'${primitive}'`);
2430+
});
2431+
2432+
it('does not flag object input with complex_data_type_name', () => {
2433+
const diagnostics = runSecurityLint(
2434+
wrap(
2435+
` order: object\n complex_data_type_name: "OrderRecord"\n`,
2436+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2437+
)
2438+
);
2439+
expect(
2440+
diagnostics.filter(
2441+
d =>
2442+
d.code === 'complex-data-type-on-primitive' ||
2443+
d.code === 'object-type-missing-schema'
2444+
)
2445+
).toHaveLength(0);
2446+
});
2447+
2448+
it('does not flag object input that uses schema:', () => {
2449+
const diagnostics = runSecurityLint(
2450+
wrap(
2451+
` order: object\n schema: "schema://order_schema"\n`,
2452+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2453+
)
2454+
);
2455+
expect(
2456+
diagnostics.filter(
2457+
d =>
2458+
d.code === 'complex-data-type-on-primitive' ||
2459+
d.code === 'object-type-missing-schema'
2460+
)
2461+
).toHaveLength(0);
2462+
});
2463+
2464+
it('does not flag list[object] output with complex_data_type_name', () => {
2465+
const diagnostics = runSecurityLint(
2466+
wrap(
2467+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`,
2468+
` items: list[object]\n complex_data_type_name: "OrderRecord"\n`
2469+
)
2470+
);
2471+
expect(
2472+
diagnostics.filter(
2473+
d =>
2474+
d.code === 'complex-data-type-on-primitive' ||
2475+
d.code === 'object-type-missing-schema'
2476+
)
2477+
).toHaveLength(0);
2478+
});
2479+
2480+
it('errors on list[string] input with complex_data_type_name', () => {
2481+
const diagnostics = runSecurityLint(
2482+
wrap(
2483+
` tags: list[string]\n complex_data_type_name: "lightning__objectType"\n`,
2484+
` ok: object\n complex_data_type_name: "lightning__objectType"\n`
2485+
)
2486+
);
2487+
const errors = diagnostics.filter(
2488+
d => d.code === 'complex-data-type-on-primitive'
2489+
);
2490+
expect(errors).toHaveLength(1);
2491+
expect(errors[0].message).toContain("'list[string]'");
2492+
});
2493+
2494+
it('reports both error and warning for mixed declarations', () => {
2495+
const diagnostics = runSecurityLint(
2496+
wrap(
2497+
` amount: number\n complex_data_type_name: "lightning__objectType"\n`,
2498+
` result: object\n description: "bare object output"\n`
2499+
)
2500+
);
2501+
const errors = diagnostics.filter(
2502+
d => d.code === 'complex-data-type-on-primitive'
2503+
);
2504+
const warnings = diagnostics.filter(
2505+
d => d.code === 'object-type-missing-schema'
2506+
);
2507+
expect(errors).toHaveLength(1);
2508+
expect(errors[0].message).toContain("'amount'");
2509+
expect(warnings).toHaveLength(1);
2510+
expect(warnings[0].message).toContain("'result'");
2511+
});
2512+
});

0 commit comments

Comments
 (0)