-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.ts
284 lines (240 loc) · 7.98 KB
/
main.ts
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import { exists as fsExists } from "https://deno.land/[email protected]/fs/mod.ts";
import { parse as parseFlags } from "https://deno.land/std/flags/mod.ts";
import { parse as parseTs } from "https://github.com/nestdotland/deno_swc/raw/master/mod.ts";
import {
ModuleItem,
Program,
Statement,
TsInterfaceDeclaration,
TsKeywordType,
} from "https://github.com/nestdotland/deno_swc/raw/master/types/options.ts";
// Parse files and generate guards
/**
* Metadata regarding an interface definition which will be relevant to type
* guard generation.
*/
export interface InterfaceDef {
/**
* Declared name of interface.
*/
name: string;
/**
* Defined properties.
*/
properties: InterfaceProp[];
}
/**
* Metadata of an interface property which will be relevant to type
* guard generation.
*/
export interface InterfaceProp {
/**
* Declared name of property.
*/
name: string;
/**
* Kind if available.
*/
kind?: string;
}
/**
* Holds a Typescript SWC AST and the associated interfaces. For use in the
* Interface.FromSrc() function.
*/
interface SrcInterfaceCollection {
/**
* Source code SWC AST.
*/
ast: ModuleItem[] | Statement[];
/**
* Parsed interfaces.
*/
interfaces: Interface[];
}
/**
* Represents an interface for which to generate type guards. Provides functions
* and methods for processing.
*/
export class Interface implements InterfaceDef {
name: string;
properties: InterfaceProp[];
/**
* Create an Interface from a TsInterfaceDeclaration SWC AST node.
* @param node SWC AST node from which to create the Interface.
* @throws {string} If node cannot be used to create an Interface.
*/
constructor(node: TsInterfaceDeclaration) {
// Set the interface name
this.name = node.id.value;
// Parse properties
this.properties = node.body.body.map((prop) => {
// Check is a property signature
if (prop.type !== "TsPropertySignature") {
throw `encountered a TsInterfaceBody item of type = '${prop.type}' but expected 'TsPropertySignature'`;
}
// Ensure we can get the property name
if (prop.key.type !== "Identifier") {
throw `encountered a TsPropertySignature with a key.type = ${prop.key.type} but expected 'Identifier'`;
}
// Get type of property if there is an annotation
let kind = undefined;
if (prop.typeAnnotation !== null && prop.typeAnnotation !== undefined) {
// What type of type annotation?
const annotation = prop.typeAnnotation.typeAnnotation;
if (annotation.type === "TsKeywordType") {
// The compiler seems to think annotation could be a TsImportType
// or a TsKeywordType even though the only type with
// .type === "TsKeywordType" is a TsKeywordType so we will manually cast.
kind = (annotation as TsKeywordType).kind;
} else {
// Throw errors when we get a type we haven't accounted for
// yet so we can add support in the future.
throw `interface property type annotation type '${prop.typeAnnotation.type}' is not supported`;
}
}
return {
name: prop.key.value,
kind: kind,
};
});
}
/**
* For an arbitrary SWC AST extract and return all Interface's. Only looks for
* interfaces at the top level, not sure if interface declarations can be nested
* in blocks but if they can this function won't find them.
* @param nodes Abstract syntax tree from which to extract.
* @returns All found interfaces.
* @throws {string} If any found TsInterfaceDeclaration nodes are not sutable
* for interface construction.
*/
static ExtractAll(nodes: ModuleItem[] | Statement[]): Interface[] {
// Filter to only grab TsInterfaceDeclaration nodes
// Typescript type narrowing doesn't work with [].filter() so we must do a
// normal loop.
let decls: TsInterfaceDeclaration[] = [];
for (let i = 0; i < nodes.length; i++) {
const item = nodes[i];
if (item.type === "TsInterfaceDeclaration") {
decls.push(item);
}
}
// Parse declarations
return decls.map((node) => {
return new Interface(node);
});
}
/**
* For a piece of Typescript source code extract all interface's.
* @param src Typescript source code text.
* @returns All found interfaces.
* @throws {string} If any found TsInterfaceDeclaration SWC AST nodes are not
* sutable for interface construction.
*/
static FromSrc(src: string): SrcInterfaceCollection {
// Parse Typescript into AST
const ast = parseTs(src, {
syntax: "typescript",
});
const nodes: (ModuleItem[] | Statement[]) = ast.body;
// Extract interfaces
const interfaces = Interface.ExtractAll(nodes);
return {
ast: nodes,
interfaces: interfaces,
};
}
}
// If running in the command line
if (import.meta.main === true) {
// Parse command line arguments
const args = parseFlags(Deno.args, {
alias: {
"h": "help",
"f": "file",
"o": "output",
"d": "debug",
},
default: {
"output": "<FILE>-guard.ts",
"debug": [],
},
});
if (args.help === true) {
console.log(`deno-guard - automatically generates Typescript guards
USAGE
deno-guard [-h,--help] -f,--file FILE -o,--output OUT_PATTERN -d,--debug DEBUG
OPTIONS
-h,--help Show help text.
-f,--file Input source files. Can be specified multiple times.
-o,--output Pattern for output file names. The string '<FILE>' will be
replaced by the name of the input file without extension.
-d,--debug Debug options, used mainly for development. Can be specified
multiple times. Only accepts the following: 'ast' (prints each
source file's AST to stdout).
BEHAVIOR
Generates Typescript guards for all interfaces in the specified file.
The checker will be a function who's name is: "is<Type name>" where the first
letter of the type name is capitalized.
`);
Deno.exit(0);
}
if (typeof args.file === "string") {
args.file = [args.file];
}
if (args.file === undefined || args.file.length === 0) {
console.error("At least one file must be specified via the -f FILE flag.");
Deno.exit(1);
}
let lostFiles: string[] = await Promise.all(args.file
.map(async (filePath: string) => {
if (await fsExists(filePath) === false) {
return filePath;
}
}));
lostFiles = lostFiles.filter((v: string) => v !== undefined);
if (lostFiles.length > 0) {
console.error(`Input file(s) not found: ${lostFiles.join(",")}`);
Deno.exit(1);
}
if (typeof args.debug === "string") {
args.debug = [args.debug];
}
let guards: string[] = await Promise.all(
args.file.map(async (fileName: string) => {
// Read file
let srcTxt = null;
try {
srcTxt = await Deno.readTextFile(fileName);
} catch (e) {
console.error(`Failed to open ${fileName}: ${e}`);
Deno.exit(1);
}
// Parse interface declarations
const srcInterfaces = Interface.FromSrc(srcTxt);
if (args.debug.indexOf("ast") !== -1) {
console.log(fileName);
console.log(JSON.stringify(srcInterfaces.ast, null, 4));
}
return srcInterfaces.interfaces.map((def) => {
const guardName = `is${def.name[0].toUpperCase() + def.name.slice(1)}`;
// For each type we will generate some custom code
// Currently we only care about: https://github.com/nestdotland/deno_swc/blob/188fb2feb8d6c4f8a663d0f6d49b65f8b8956369/types/options.ts#L1778
let checks = []; // Note: the order here matters
return `\
/**
* Ensures that value is a ${def.name} interface.
* @param value To check.
* @returns True if value is ${def.name}, false otherwise.
*/
function ${guardName}(value: unknown) value is ${def.name} {
// TODO: Write type guards here
}
`;
});
}),
);
const enc = new TextEncoder();
guards.forEach((guard: string) => {
Deno.stdout.write(enc.encode(guard.toString()));
});
}