-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgetFlowValidation.ts
More file actions
130 lines (112 loc) · 4.97 KB
/
Copy pathgetFlowValidation.ts
File metadata and controls
130 lines (112 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import {flattenDiagnosticMessageText} from "typescript";
import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types";
import {createCompilerHost, generateFlowSourceCode, ValidationResult} from "../utils";
/**
* Validates a flow by generating virtual TypeScript code and running it through the TS compiler.
*/
export const getFlowValidation = (
flow?: Flow,
functions?: FunctionDefinition[],
dataTypes?: DataType[]
): ValidationResult => {
if (!flow?.startingNodeId) {
return {
isValid: false,
returnType: "void",
diagnostics: [{
nodeId: null,
parameterIndex: null,
code: 0,
message: "You need to provide a starting node to be able to execute this flow.",
severity: "error",
}]
}
}
if (!flow.nodes?.nodes?.find(n => n?.id == flow.startingNodeId)) {
return {
isValid: false,
returnType: "void",
diagnostics: [{
nodeId: null,
parameterIndex: null,
code: 0,
message: "The starting node is not linked within the flow. Please make sure the starting node is connected to the rest of the flow.",
severity: "error",
}]
}
}
const functionIdentifiers = new Set(functions?.map(f => f.identifier));
const unreachableFunctionDiagnostics = (flow.nodes?.nodes ?? [])
.filter(n => n?.functionDefinition && !functionIdentifiers.has(n.functionDefinition.identifier))
.map(n => ({
nodeId: n!.id,
parameterIndex: null,
code: 0,
message: `The function definition "${n!.functionDefinition!.identifier}" is not reachable.`,
severity: "error" as const,
}));
if (unreachableFunctionDiagnostics.length > 0) {
return {
isValid: false,
returnType: "void",
diagnostics: unreachableFunctionDiagnostics,
}
}
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes);
// 3. Virtual TypeScript Compilation
const fileName = "index.ts";
const host = createCompilerHost(fileName, sourceCode);
const sourceFile = host.getSourceFile(fileName)!;
const program = host.languageService.getProgram()!;
const diagnostics = program.getSemanticDiagnostics(sourceFile);
const errors = diagnostics.map(d => {
const message = flattenDiagnosticMessageText(d.messageText, "\n");
// "Argument of type 'undefined' is not assignable to parameter of type 'number'."
// We ignore this in flow validation too because we might generate code for incomplete flows.
let nodeId: NodeFunction['id'] | undefined;
let parameterIndex: number | null = null;
if (d.start !== undefined) {
const fullText = sourceFile.getFullText();
// Search for position marker comment near the error location
// The error position is typically the start of the problematic token (e.g., "undefined")
const searchStart = Math.max(0, d.start - 300);
const searchEnd = Math.min(fullText.length, d.start);
const searchText = fullText.substring(searchStart, searchEnd);
// Find all @pos comments in the search range
const posRegex = /\/\* @pos ([^ ]+) (\d+|null) \*\//g;
let match;
let closestMatch: RegExpExecArray | null = null;
let closestCommentEnd = -1;
// Collect all matches and find the one whose end is closest to d.start
// We want the comment that is immediately before the error
while ((match = posRegex.exec(searchText)) !== null) {
const commentStart = searchStart + match.index;
const commentEnd = commentStart + match[0].length;
// Only consider comments that end before or very close to the error start
// This ensures we get the @pos comment that directly precedes the problematic argument
if (commentEnd <= d.start!) {
if (commentEnd > closestCommentEnd) {
closestCommentEnd = commentEnd;
closestMatch = match;
}
}
}
if (closestMatch) {
nodeId = closestMatch[1] === "null" ? null : closestMatch[1] as NodeFunction['id'];
parameterIndex = parseInt(closestMatch[2], 10);
}
}
return {
message,
code: d.code,
severity: "error" as const,
nodeId,
parameterIndex: typeof parameterIndex == "number" && Number.isSafeInteger(parameterIndex) ? parameterIndex : null,
};
}).filter((e) => e !== null);
return {
isValid: !errors.some(e => e?.severity === "error"),
returnType: "void",
diagnostics: errors,
};
};