Skip to content

Commit e20b4ff

Browse files
authored
Add "use step" and "use workflow" typo detection and link to documentation (#88)
1 parent dbf2207 commit e20b4ff

File tree

7 files changed

+660
-1
lines changed

7 files changed

+660
-1
lines changed

.changeset/tricky-wasps-ask.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/typescript-plugin": patch
3+
---
4+
5+
Add "use step" and "use workflow" typo detection and link to documentation
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import ts from 'typescript/lib/tsserverlibrary';
2+
import { describe, expect, it } from 'vitest';
3+
import { getCodeFixes } from './code-fixes';
4+
import { createTestProgram } from './test-helpers';
5+
6+
describe('getCodeFixes', () => {
7+
describe('Code 9008: Directive typo fixes', () => {
8+
it('provides a fix for "use workfow" typo', () => {
9+
const source = `
10+
export async function myWorkflow() {
11+
'use workfow';
12+
return 123;
13+
}
14+
`;
15+
16+
const { program, sourceFile } = createTestProgram(source);
17+
18+
// Find the position of 'use workfow'
19+
const typoStart = sourceFile.text.indexOf("'use workfow'");
20+
const typoEnd = typoStart + "'use workfow'".length;
21+
22+
const fixes = getCodeFixes(
23+
'test.ts',
24+
typoStart,
25+
typoEnd,
26+
9008,
27+
program,
28+
ts
29+
);
30+
31+
expect(fixes.length).toBe(1);
32+
expect(fixes[0].description).toContain('use workflow');
33+
expect(fixes[0].fixName).toBe('fix-directive-typo');
34+
});
35+
36+
it('provides a fix for "use ste" typo', () => {
37+
const source = `
38+
export async function myStep() {
39+
'use ste';
40+
return 'hello';
41+
}
42+
`;
43+
44+
const { program, sourceFile } = createTestProgram(source);
45+
46+
const typoStart = sourceFile.text.indexOf("'use ste'");
47+
const typoEnd = typoStart + "'use ste'".length;
48+
49+
const fixes = getCodeFixes(
50+
'test.ts',
51+
typoStart,
52+
typoEnd,
53+
9008,
54+
program,
55+
ts
56+
);
57+
58+
expect(fixes.length).toBe(1);
59+
expect(fixes[0].description).toContain('use step');
60+
});
61+
62+
it('replaces typo with correct directive preserving quotes', () => {
63+
const source = `
64+
export async function myWorkflow() {
65+
'use workflw';
66+
return 123;
67+
}
68+
`;
69+
70+
const { program, sourceFile } = createTestProgram(source);
71+
72+
const typoStart = sourceFile.text.indexOf("'use workflw'");
73+
const typoEnd = typoStart + "'use workflw'".length;
74+
75+
const fixes = getCodeFixes(
76+
'test.ts',
77+
typoStart,
78+
typoEnd,
79+
9008,
80+
program,
81+
ts
82+
);
83+
84+
expect(fixes.length).toBe(1);
85+
const change = fixes[0].changes[0].textChanges[0];
86+
expect(change.newText).toBe("'use workflow'");
87+
});
88+
89+
it('works with double quotes', () => {
90+
const source = `
91+
export async function myStep() {
92+
"use ste";
93+
return 'hello';
94+
}
95+
`;
96+
97+
const { program, sourceFile } = createTestProgram(source);
98+
99+
const typoStart = sourceFile.text.indexOf('"use ste"');
100+
const typoEnd = typoStart + '"use ste"'.length;
101+
102+
const fixes = getCodeFixes(
103+
'test.ts',
104+
typoStart,
105+
typoEnd,
106+
9008,
107+
program,
108+
ts
109+
);
110+
111+
expect(fixes.length).toBe(1);
112+
const change = fixes[0].changes[0].textChanges[0];
113+
expect(change.newText).toBe('"use step"');
114+
});
115+
});
116+
117+
describe('Non-typo error codes', () => {
118+
it('does not provide fixes for other error codes', () => {
119+
const source = `
120+
export async function myWorkflow() {
121+
'use workflow';
122+
return 123;
123+
}
124+
`;
125+
126+
const { program } = createTestProgram(source);
127+
128+
const start = 0;
129+
const end = 10;
130+
131+
// Try code 9001 (not a typo)
132+
const fixes = getCodeFixes('test.ts', start, end, 9001, program, ts);
133+
134+
expect(fixes.length).toBe(0);
135+
});
136+
});
137+
138+
describe('Edge cases', () => {
139+
it('does not crash on invalid position', () => {
140+
const source = `
141+
export async function myWorkflow() {
142+
'use workflow';
143+
return 123;
144+
}
145+
`;
146+
147+
const { program } = createTestProgram(source);
148+
149+
const fixes = getCodeFixes('test.ts', 999999, 999999, 9008, program, ts);
150+
151+
expect(fixes.length).toBe(0);
152+
});
153+
});
154+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type {
2+
CodeFixAction,
3+
FileTextChanges,
4+
Node,
5+
Program,
6+
} from 'typescript/lib/tsserverlibrary';
7+
import { getDirectiveTypo } from './utils';
8+
9+
type TypeScriptLib = typeof import('typescript/lib/tsserverlibrary');
10+
11+
export function getCodeFixes(
12+
fileName: string,
13+
start: number,
14+
end: number,
15+
errorCode: number,
16+
program: Program,
17+
ts: TypeScriptLib
18+
): CodeFixAction[] {
19+
const fixes: CodeFixAction[] = [];
20+
21+
// Only provide fixes for typo errors (code 9008)
22+
if (errorCode !== 9008) {
23+
return fixes;
24+
}
25+
26+
const sourceFile = program.getSourceFile(fileName);
27+
if (!sourceFile) {
28+
return fixes;
29+
}
30+
31+
// Find the string literal at the error position
32+
let stringNode: Node | undefined;
33+
34+
function visit(node: Node) {
35+
if (
36+
ts.isStringLiteral(node) &&
37+
node.getStart(sourceFile) <= start &&
38+
node.getEnd() >= end
39+
) {
40+
stringNode = node;
41+
return;
42+
}
43+
ts.forEachChild(node, visit);
44+
}
45+
46+
visit(sourceFile);
47+
48+
if (!stringNode || !ts.isStringLiteral(stringNode)) {
49+
return fixes;
50+
}
51+
52+
const typoText = stringNode.text;
53+
const expectedDirective = getDirectiveTypo(typoText);
54+
55+
if (!expectedDirective) {
56+
return fixes;
57+
}
58+
59+
// Get the quote character from the source
60+
const sourceText = sourceFile.text;
61+
const nodeStart = stringNode.getStart(sourceFile);
62+
const quoteChar = sourceText[nodeStart];
63+
64+
// Create a fix that replaces the typo with the correct directive
65+
const change: FileTextChanges = {
66+
fileName,
67+
textChanges: [
68+
{
69+
span: {
70+
start: nodeStart,
71+
length: stringNode.getWidth(sourceFile),
72+
},
73+
newText: `${quoteChar}${expectedDirective}${quoteChar}`,
74+
},
75+
],
76+
};
77+
78+
fixes.push({
79+
fixName: 'fix-directive-typo',
80+
description: `Replace with '${expectedDirective}'`,
81+
changes: [change],
82+
});
83+
84+
return fixes;
85+
}

0 commit comments

Comments
 (0)