Skip to content
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
4 changes: 2 additions & 2 deletions apps/docs/docs/architecture/diagnostic-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ General-purpose factory for schema validation diagnostics. Accepts a `Range`, `S

Parser-level errors only. Source is always `'parser'`. Code must be `'syntax-error'` or `'missing-token'`.

### `undefinedReferenceDiagnostic(range, message, referenceName, suggestion?, expected?)`
### `undefinedReferenceDiagnostic(range, message, referenceName, found, suggestion?, expected?)`

For undefined reference errors. Automatically appends "Did you mean?" hints via `formatSuggestionHint()`.
For undefined reference errors. Automatically appends "Did you mean?" hints via `formatSuggestionHint()`. `found` is the misspelled segment (used by the LSP quick fix).

### `typeMismatchDiagnostic(range, message, expectedType, actualType, source?)`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.members
);
Expand All @@ -582,6 +583,7 @@ function formatResolutionDiagnostic(
range,
`'@${namespace}' is not a recognized namespace`,
referenceName,
namespace,
suggestion,
result.knownNamespaces
);
Expand All @@ -592,7 +594,8 @@ function formatResolutionDiagnostic(
range,
`'@${namespace}' cannot be used as a reference. ` +
`This namespace is scoped to its parent block and is not directly referenceable`,
referenceName
referenceName,
namespace
);

case 'colinear-miss': {
Expand All @@ -601,6 +604,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.members
);
Expand All @@ -612,6 +616,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.candidates
);
Expand Down
2 changes: 2 additions & 0 deletions packages/language/src/core/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function undefinedReferenceDiagnostic(
range: Range,
message: string,
referenceName: string,
found: string,
suggestion?: string,
expected?: string[]
): Diagnostic {
Expand All @@ -70,6 +71,7 @@ export function undefinedReferenceDiagnostic(
source: 'agentscript-lint',
data: {
referenceName,
found,
...(suggestion ? { suggestion } : {}),
...(expected && expected.length > 0 ? { expected } : {}),
},
Expand Down
9 changes: 8 additions & 1 deletion packages/language/src/lint/undefined-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.members
);
Expand All @@ -618,6 +619,7 @@ function formatResolutionDiagnostic(
range,
`'@${namespace}' is not a recognized namespace`,
referenceName,
namespace,
suggestion,
result.knownNamespaces
);
Expand All @@ -628,7 +630,8 @@ function formatResolutionDiagnostic(
range,
`'@${namespace}' cannot be used as a reference. ` +
`This namespace is scoped to its parent block and is not directly referenceable`,
referenceName
referenceName,
namespace
);

case 'colinear-miss': {
Expand All @@ -637,6 +640,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.members
);
Expand All @@ -648,6 +652,7 @@ function formatResolutionDiagnostic(
range,
`'${property}' is not defined in ${namespace}`,
referenceName,
property,
suggestion,
result.candidates
);
Expand Down Expand Up @@ -802,6 +807,7 @@ function resolveNestedGlobalCheck(
cst.range,
`'${check.member}' is not defined in ${namespaceLabel}`,
referenceName,
check.member,
suggestion,
members
);
Expand Down Expand Up @@ -840,6 +846,7 @@ function resolveListElementAttrCheck(
cst.range,
`'${check.attr}' is not defined on ${namespaceLabel} elements`,
referenceName,
check.attr,
suggestion,
members
);
Expand Down
54 changes: 54 additions & 0 deletions packages/lsp/src/providers/code-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,57 @@ access:
expect(config).not.toContain('default_agent_user');
});
});

/** Return the undefined-reference quick fixes offered for `source`. */
function getUndefinedReferenceActions(source: string) {
const state = createState(source);
const diags = state.diagnostics.filter(d => d.code === 'undefined-reference');
const actions = provideCodeActions(
state,
{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
diags
);
return { diags, actions };
}

describe('undefined-reference quick-fix', () => {
const agent = (ref: string) => `config:
developer_name: "agent"
agent_type: "AgentforceServiceAgent"

variables:
customer_name: string = ""

start_agent topic_selector:
description: "route"
reasoning:
instructions: ->
| Hello {!${ref}}
`;

test('replaces a misspelled member', () => {
const source = agent('@variables.custmer_name');
const { actions } = getUndefinedReferenceActions(source);
expect(actions).toHaveLength(1);
expect(actions[0].title).toBe("Change to 'customer_name'");
const edits = Object.values(actions[0].edit!.changes!)[0];
expect(applyEdits(source, edits)).toBe(agent('@variables.customer_name'));
});

test('replaces a misspelled namespace', () => {
const source = agent('@varibles.customer_name');
const { actions } = getUndefinedReferenceActions(source);
expect(actions).toHaveLength(1);
expect(actions[0].title).toBe("Change to 'variables'");
const edits = Object.values(actions[0].edit!.changes!)[0];
expect(applyEdits(source, edits)).toBe(agent('@variables.customer_name'));
});

test('offers nothing when there is no close match', () => {
const { diags, actions } = getUndefinedReferenceActions(
agent('@variables.zzzzzzzz')
);
expect(diags).toHaveLength(1);
expect(actions).toHaveLength(0);
});
});
52 changes: 52 additions & 0 deletions packages/lsp/src/providers/code-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,58 @@ export function provideCodeActions(
});
}

// Quick fix for a misspelled `@namespace.member` reference. The range
// covers the whole reference, so only the misspelled segment is replaced:
// the namespace (right after the `@`) or the last member.
if (diagnostic.code === 'undefined-reference') {
const found = diagnostic.data?.found as string | undefined;
const suggestion = diagnostic.data?.suggestion as string | undefined;
const referenceName = diagnostic.data?.referenceName as
| string
| undefined;
if (!found || !suggestion || !referenceName) continue;

const { start, end } = diagnostic.range;
if (start.line !== end.line) continue;

// `@foo.foo` can't tell the namespace from the member; offer nothing.
if (referenceName === `@${found}.${found}`) continue;
const isNamespace = referenceName.startsWith(`@${found}.`);
const foundStart = isNamespace
? start.character + 1
: end.character - found.length;
const line = source.split('\n')[start.line];
if (
line === undefined ||
line.slice(foundStart, foundStart + found.length) !== found
) {
continue;
}

actions.push({
title: `Change to '${suggestion}'`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: true,
edit: {
changes: {
[uri]: [
{
range: {
start: { line: start.line, character: foundStart },
end: {
line: start.line,
character: foundStart + found.length,
},
},
newText: suggestion,
},
],
},
},
});
}

// Quick fix for unknown dialect — offer each available dialect
if (diagnostic.code === 'unknown-dialect') {
const availableNames = diagnostic.data?.availableNames as
Expand Down
Loading