Skip to content

Commit 79f4b5d

Browse files
authored
feat: report error when @utils.setVariables assigns to undefined variable (#59)
1 parent 6748f5a commit 79f4b5d

4 files changed

Lines changed: 372 additions & 1 deletion

File tree

dialect/agentscript/src/lint/passes/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { reasoningActionsAnalyzer } from './reasoning-actions.js';
2828
import { actionIoRule } from './action-io.js';
2929
import { actionTypeCheckRule } from './action-type-check.js';
3030
import { availableWhenTypeCheckRule } from './available-when-type-check.js';
31+
import { setVariablesIoRule } from './set-variables-io.js';
3132

3233
export { typeMapAnalyzer, typeMapKey } from './type-map.js';
3334
export type {
@@ -46,10 +47,15 @@ export {
4647
reasoningActionsAnalyzer,
4748
reasoningActionsKey,
4849
} from './reasoning-actions.js';
49-
export type { ReasoningActionEntry } from './reasoning-actions.js';
50+
export type {
51+
ReasoningActionEntry,
52+
SetVariablesEntry,
53+
} from './reasoning-actions.js';
54+
export { setVariablesEntriesKey } from './reasoning-actions.js';
5055
export { actionIoRule } from './action-io.js';
5156
export { actionTypeCheckRule } from './action-type-check.js';
5257
export { availableWhenTypeCheckRule } from './available-when-type-check.js';
58+
export { setVariablesIoRule } from './set-variables-io.js';
5359

5460
/** All AgentScript lint passes in engine execution order. */
5561
export function defaultRules(): LintPass[] {
@@ -77,5 +83,6 @@ export function defaultRules(): LintPass[] {
7783
actionIoRule(),
7884
actionTypeCheckRule(),
7985
availableWhenTypeCheckRule(),
86+
setVariablesIoRule(),
8087
];
8188
}

dialect/agentscript/src/lint/passes/reasoning-actions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ interface RawReasoningAction {
6060
export const reasoningActionsKey =
6161
storeKey<ReasoningActionEntry[]>('reasoning-actions');
6262

63+
export interface SetVariablesEntry {
64+
topicName: string;
65+
/** The ReasoningActionBlock AST node. */
66+
ra: Record<string, unknown>;
67+
/** WithClause statements within the setVariables block. */
68+
statements: Array<Record<string, unknown>> | undefined;
69+
/** Range of the @utils.setVariables expression. */
70+
actionRefRange: Range | undefined;
71+
}
72+
73+
export const setVariablesEntriesKey = storeKey<SetVariablesEntry[]>(
74+
'set-variables-entries'
75+
);
76+
6377
class ReasoningActionsAnalyzer implements LintPass {
6478
readonly id = reasoningActionsKey;
6579
readonly description =
@@ -74,6 +88,7 @@ class ReasoningActionsAnalyzer implements LintPass {
7488
if (!ctx) return;
7589

7690
const raw: RawReasoningAction[] = [];
91+
const setVarEntries: SetVariablesEntry[] = [];
7792
const rootObj = root as AstNodeLike;
7893

7994
// Support both 'subagent' (base dialect) and 'topic' (agentforce dialect)
@@ -155,6 +170,12 @@ class ReasoningActionsAnalyzer implements LintPass {
155170
statements,
156171
actionRefRange,
157172
});
173+
} else if (
174+
decomposed &&
175+
decomposed.namespace === 'utils' &&
176+
decomposed.property === 'setVariables'
177+
) {
178+
setVarEntries.push({ topicName, ra, statements, actionRefRange });
158179
}
159180
}
160181
}
@@ -187,6 +208,7 @@ class ReasoningActionsAnalyzer implements LintPass {
187208
}
188209

189210
store.set(reasoningActionsKey, entries);
211+
store.set(setVariablesEntriesKey, setVarEntries);
190212
}
191213
}
192214

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2026, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
* For full license text, see the LICENSE file in the repo root or https://www.apache.org/licenses/LICENSE-2.0
6+
*/
7+
8+
/**
9+
* setVariables I/O validation — validates that `with` clause parameters in
10+
* @utils.setVariables reasoning actions reference defined mutable variables.
11+
*
12+
* Diagnostics: set-variables-unknown-variable, set-variables-immutable-target
13+
*/
14+
15+
import type { LintPass } from '@agentscript/language';
16+
import {
17+
defineRule,
18+
each,
19+
attachDiagnostic,
20+
findSuggestion,
21+
lintDiagnostic,
22+
} from '@agentscript/language';
23+
import type { CstMeta, SyntaxNode } from '@agentscript/types';
24+
import { toRange, DiagnosticSeverity } from '@agentscript/types';
25+
import { setVariablesEntriesKey } from './reasoning-actions.js';
26+
import { typeMapKey } from './type-map.js';
27+
28+
export function setVariablesIoRule(): LintPass {
29+
return defineRule({
30+
id: 'set-variables-io',
31+
description:
32+
'Validates with clause params in @utils.setVariables reference defined mutable variables',
33+
deps: { entry: each(setVariablesEntriesKey), typeMap: typeMapKey },
34+
35+
run({ entry, typeMap }) {
36+
const { statements } = entry;
37+
if (!statements) return;
38+
39+
for (const stmt of statements) {
40+
if (stmt.__kind !== 'WithClause') continue;
41+
const param = stmt.param as string;
42+
if (!param) continue;
43+
44+
const varInfo = typeMap.variables.get(param);
45+
if (!varInfo) {
46+
const cst = stmt.__cst as CstMeta | undefined;
47+
if (!cst) continue;
48+
const paramCstNode = (stmt as { __paramCstNode?: SyntaxNode })
49+
.__paramCstNode;
50+
const range = paramCstNode ? toRange(paramCstNode) : cst.range;
51+
52+
const suggestion = findSuggestion(param, [
53+
...typeMap.variables.keys(),
54+
]);
55+
const msg = `'${param}' is not a defined variable. @utils.setVariables can only assign to declared variables.`;
56+
attachDiagnostic(
57+
stmt,
58+
lintDiagnostic(
59+
range,
60+
msg,
61+
DiagnosticSeverity.Error,
62+
'set-variables-unknown-variable',
63+
{ suggestion }
64+
)
65+
);
66+
continue;
67+
}
68+
69+
if (varInfo.modifier !== 'mutable') {
70+
const cst = stmt.__cst as CstMeta | undefined;
71+
if (!cst) continue;
72+
const paramCstNode = (stmt as { __paramCstNode?: SyntaxNode })
73+
.__paramCstNode;
74+
const range = paramCstNode ? toRange(paramCstNode) : cst.range;
75+
76+
const qualifier = varInfo.modifier ?? 'non-mutable';
77+
const msg = `'${param}' is a ${qualifier} variable. @utils.setVariables can only assign to mutable variables.`;
78+
attachDiagnostic(
79+
stmt,
80+
lintDiagnostic(
81+
range,
82+
msg,
83+
DiagnosticSeverity.Error,
84+
'set-variables-immutable-target'
85+
)
86+
);
87+
}
88+
}
89+
},
90+
});
91+
}

0 commit comments

Comments
 (0)