sql-linter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/package.json +25 -0
- package/src/index.ts +527 -0
- package/tsconfig.json +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { asyncDescribeQueries, } from "sql-describe";
|
|
7
|
+
import ts from "typescript";
|
|
8
|
+
const { values, positionals } = parseArgs({
|
|
9
|
+
args: process.argv.slice(2),
|
|
10
|
+
options: {
|
|
11
|
+
db: {
|
|
12
|
+
type: "string",
|
|
13
|
+
default: "postgres://postgres:postgres@localhost:5432/postgres",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
allowPositionals: true,
|
|
17
|
+
});
|
|
18
|
+
const dir = positionals[0];
|
|
19
|
+
if (!dir) {
|
|
20
|
+
console.error("Usage: sql-linter <dir> [--db <connection-url>]");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const targetDir = dir;
|
|
24
|
+
const dbUrl = values.db;
|
|
25
|
+
async function walkInner(dir, results) {
|
|
26
|
+
const files = await fs.readdir(dir);
|
|
27
|
+
await Promise.all(files.map(async (f) => {
|
|
28
|
+
const file = path.resolve(dir, f);
|
|
29
|
+
const stat = await fs.stat(file);
|
|
30
|
+
if (stat?.isDirectory()) {
|
|
31
|
+
await walkInner(file, results);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
results.push(file);
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
async function walk(dir) {
|
|
40
|
+
const results = [];
|
|
41
|
+
await walkInner(dir, results);
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
async function lintNode(node, callback, program) {
|
|
45
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
46
|
+
await callback(node, program);
|
|
47
|
+
}
|
|
48
|
+
const promises = [];
|
|
49
|
+
ts.forEachChild(node, (n) => {
|
|
50
|
+
promises.push(lintNode(n, callback, program));
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(promises);
|
|
53
|
+
}
|
|
54
|
+
async function lint(dir, callback) {
|
|
55
|
+
const fileNames = await walk(dir);
|
|
56
|
+
const program = ts.createProgram(fileNames, {
|
|
57
|
+
target: ts.ScriptTarget.ES2015,
|
|
58
|
+
});
|
|
59
|
+
await Promise.all(fileNames.map(async (fileName) => {
|
|
60
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
61
|
+
if (!sourceFile)
|
|
62
|
+
return;
|
|
63
|
+
await lintNode(sourceFile, callback, program);
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
function pgTypeToTsInnerForParams(pg_type, typeChecker) {
|
|
67
|
+
if (typeof pg_type === "string") {
|
|
68
|
+
switch (pg_type) {
|
|
69
|
+
case "Bool":
|
|
70
|
+
return typeChecker.getBooleanType();
|
|
71
|
+
case "Float4":
|
|
72
|
+
case "Float8":
|
|
73
|
+
case "Int2":
|
|
74
|
+
case "Int4":
|
|
75
|
+
return typeChecker.getNumberType();
|
|
76
|
+
case "Int8":
|
|
77
|
+
case "Numeric":
|
|
78
|
+
return typeChecker.getUnionType([
|
|
79
|
+
typeChecker.getNumberType(),
|
|
80
|
+
typeChecker.getBigIntType(),
|
|
81
|
+
]);
|
|
82
|
+
case "Interval":
|
|
83
|
+
case "Text":
|
|
84
|
+
case "Varchar":
|
|
85
|
+
case "Time":
|
|
86
|
+
case "Macaddr":
|
|
87
|
+
case "Uuid":
|
|
88
|
+
return typeChecker.getStringType();
|
|
89
|
+
case "Date":
|
|
90
|
+
case "Timestamp":
|
|
91
|
+
case "Timestamptz": {
|
|
92
|
+
const dateSymbol = typeChecker.resolveName("Date", undefined, ts.SymbolFlags.Type, false);
|
|
93
|
+
const dateType = typeChecker.getDeclaredTypeOfSymbol(dateSymbol);
|
|
94
|
+
return typeChecker.getUnionType([
|
|
95
|
+
typeChecker.getStringType(),
|
|
96
|
+
dateType,
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
case "TextArray":
|
|
100
|
+
return typeChecker.createArrayType(typeChecker.getStringType());
|
|
101
|
+
case "Float4Array":
|
|
102
|
+
case "Float8Array":
|
|
103
|
+
case "Int2Array":
|
|
104
|
+
case "Int4Array":
|
|
105
|
+
case "Int8Array":
|
|
106
|
+
return typeChecker.createArrayType(typeChecker.getUnionType([
|
|
107
|
+
typeChecker.getNumberType(),
|
|
108
|
+
typeChecker.createArrayType(typeChecker.getNumberType()),
|
|
109
|
+
]));
|
|
110
|
+
case "Jsonb":
|
|
111
|
+
return typeChecker.getAnyType();
|
|
112
|
+
}
|
|
113
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
114
|
+
}
|
|
115
|
+
if ("Custom" in pg_type) {
|
|
116
|
+
if (typeof pg_type.Custom.kind !== "string") {
|
|
117
|
+
if ("Enum" in pg_type.Custom.kind) {
|
|
118
|
+
return typeChecker.getStringType();
|
|
119
|
+
}
|
|
120
|
+
else if ("Array" in pg_type.Custom.kind) {
|
|
121
|
+
return typeChecker.createArrayType(pgTypeToTsInnerForParams(pg_type.Custom.kind.Array, typeChecker));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
126
|
+
}
|
|
127
|
+
function pgTypeToTsMappingForParams(pg_type, nullable, typeChecker) {
|
|
128
|
+
const inner = pgTypeToTsInnerForParams(pg_type, typeChecker);
|
|
129
|
+
if (nullable === true) {
|
|
130
|
+
return typeChecker.getNullableType(inner, ts.TypeFlags.Null);
|
|
131
|
+
}
|
|
132
|
+
return inner;
|
|
133
|
+
}
|
|
134
|
+
function pgTypeToTsForColumns(pg_type, typeChecker) {
|
|
135
|
+
if (typeof pg_type === "string") {
|
|
136
|
+
switch (pg_type) {
|
|
137
|
+
case "Bool":
|
|
138
|
+
return [typeChecker.getBooleanType()];
|
|
139
|
+
case "Float4":
|
|
140
|
+
case "Float8":
|
|
141
|
+
case "Int2":
|
|
142
|
+
case "Int4":
|
|
143
|
+
return [typeChecker.getNumberType()];
|
|
144
|
+
case "Int8":
|
|
145
|
+
case "Numeric":
|
|
146
|
+
return [typeChecker.getStringType()];
|
|
147
|
+
case "TstzRange":
|
|
148
|
+
case "Uuid":
|
|
149
|
+
case "Macaddr":
|
|
150
|
+
case "Text":
|
|
151
|
+
case "Varchar":
|
|
152
|
+
return [typeChecker.getStringType()];
|
|
153
|
+
case "Date":
|
|
154
|
+
case "Timestamp":
|
|
155
|
+
case "Timestamptz": {
|
|
156
|
+
const dateSymbol = typeChecker.resolveName("Date", undefined, ts.SymbolFlags.Type, false);
|
|
157
|
+
const dateType = typeChecker.getDeclaredTypeOfSymbol(dateSymbol);
|
|
158
|
+
return [dateType];
|
|
159
|
+
}
|
|
160
|
+
case "TextArray":
|
|
161
|
+
return [
|
|
162
|
+
typeChecker.createArrayType(typeChecker.getStringType()),
|
|
163
|
+
];
|
|
164
|
+
case "Float4Array":
|
|
165
|
+
case "Float8Array":
|
|
166
|
+
case "Int2Array":
|
|
167
|
+
case "Int4Array":
|
|
168
|
+
case "Int8Array":
|
|
169
|
+
return [
|
|
170
|
+
typeChecker.createArrayType(typeChecker.getNumberType()),
|
|
171
|
+
typeChecker.createArrayType(typeChecker.createArrayType(typeChecker.getNumberType())),
|
|
172
|
+
];
|
|
173
|
+
case "JsonArray":
|
|
174
|
+
case "JsonbArray":
|
|
175
|
+
case "Json":
|
|
176
|
+
case "Jsonb":
|
|
177
|
+
return [typeChecker.getAnyType()];
|
|
178
|
+
}
|
|
179
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
180
|
+
}
|
|
181
|
+
if ("Custom" in pg_type) {
|
|
182
|
+
if (typeof pg_type.Custom.kind !== "string") {
|
|
183
|
+
if ("Enum" in pg_type.Custom.kind) {
|
|
184
|
+
return [
|
|
185
|
+
typeChecker.getUnionType(pg_type.Custom.kind.Enum.map((v) => typeChecker.getStringLiteralType(v))),
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
else if ("Array" in pg_type.Custom.kind) {
|
|
189
|
+
return [
|
|
190
|
+
typeChecker.createArrayType(pgTypeToTsInnerForParams(pg_type.Custom.kind.Array, typeChecker)),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
196
|
+
}
|
|
197
|
+
function checkQueryParams(givenParams, expectedParams, argNames, typeChecker) {
|
|
198
|
+
if (expectedParams == null) {
|
|
199
|
+
if (givenParams != null) {
|
|
200
|
+
return {
|
|
201
|
+
node: givenParams,
|
|
202
|
+
message: "This query takes no params, but you supplied some here",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if ("Right" in expectedParams) {
|
|
208
|
+
throw new Error("This should only happen for mssql");
|
|
209
|
+
}
|
|
210
|
+
if (givenParams == null) {
|
|
211
|
+
if (expectedParams == null || expectedParams.Left.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return { message: "This query expects parameters, but none were given" };
|
|
215
|
+
}
|
|
216
|
+
const expectedParameters = expectedParams.Left;
|
|
217
|
+
const givenParameters = {};
|
|
218
|
+
if (argNames != null) {
|
|
219
|
+
const t = typeChecker.getTypeAtLocation(givenParams);
|
|
220
|
+
for (const property of t.getProperties()) {
|
|
221
|
+
const t = typeChecker.getTypeOfSymbol(property);
|
|
222
|
+
givenParameters[property.escapedName.toString()] = t;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
if (!ts.isArrayLiteralExpression(givenParams)) {
|
|
227
|
+
return {
|
|
228
|
+
node: givenParams,
|
|
229
|
+
message: "The arguments to the call should be an array e.g.: `[value1, value2]`",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
givenParams.elements.forEach((e, i) => {
|
|
234
|
+
if (e == null)
|
|
235
|
+
return;
|
|
236
|
+
const t = typeChecker.getTypeAtLocation(e);
|
|
237
|
+
givenParameters[i] = t;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let i = 0;
|
|
242
|
+
for (const expectedParam of expectedParameters) {
|
|
243
|
+
const name = argNames?.[i] || i;
|
|
244
|
+
const eName = argNames?.[i] || i + 1;
|
|
245
|
+
const paramType = givenParameters[name];
|
|
246
|
+
try {
|
|
247
|
+
const expected = pgTypeToTsMappingForParams(expectedParam, false, typeChecker);
|
|
248
|
+
if (!typeChecker.isTypeAssignableTo(paramType, typeChecker.getNullableType(expected, ts.TypeFlags.Undefined))) {
|
|
249
|
+
return {
|
|
250
|
+
message: `Expected a ${typeChecker.typeToString(expected)} for $${eName}, found a ${typeChecker.typeToString(paramType)}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
return {
|
|
256
|
+
message: e,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
i += 1;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
let count = 0;
|
|
264
|
+
function report(node, message) {
|
|
265
|
+
count++;
|
|
266
|
+
const sourceFile = node.getSourceFile();
|
|
267
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
268
|
+
console.log(`${sourceFile.fileName}:${line}:${character}:
|
|
269
|
+
${chalk.red(message)}
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
async function main() {
|
|
273
|
+
await lint(targetDir, async (node, program) => {
|
|
274
|
+
let typeChecker = program.getTypeChecker();
|
|
275
|
+
if (!ts.isIdentifier(node.tag) || node.tag.escapedText !== "sql")
|
|
276
|
+
return;
|
|
277
|
+
let sqlStatement = node.template.rawText;
|
|
278
|
+
if (sqlStatement == null || sqlStatement.length === 0) {
|
|
279
|
+
report(node, "Empty sql query");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const parent = node.parent;
|
|
283
|
+
if (!ts.isCallExpression(parent)) {
|
|
284
|
+
report(parent, "The parent of a sql`...` call should be a Call expression.");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (parent.arguments.length > 2) {
|
|
288
|
+
report(parent, "Expected at most 2 arguments to this call");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const params = parent.arguments[1];
|
|
292
|
+
const usesRemappings = sqlStatement.includes("$/") || sqlStatement.includes("$(");
|
|
293
|
+
let description;
|
|
294
|
+
let argNames = null;
|
|
295
|
+
sqlStatement = sqlStatement
|
|
296
|
+
.replaceAll(":csv", "")
|
|
297
|
+
.replaceAll(/[^:]:json/g, "");
|
|
298
|
+
if (!usesRemappings) {
|
|
299
|
+
const descriptions = await asyncDescribeQueries(dbUrl, [sqlStatement]);
|
|
300
|
+
const first = descriptions[0];
|
|
301
|
+
if ("error" in first) {
|
|
302
|
+
report(node, `DBError: ${first.error}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
description = first.description;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
argNames = [];
|
|
311
|
+
let prefix = "/";
|
|
312
|
+
let suffix = "/";
|
|
313
|
+
let counter = 1;
|
|
314
|
+
const normalizedStatement = sqlStatement.replaceAll(/\$[\/\(]\s*([a-zA-Z0-9_]*)\s*[\/\)]/g, (substring, name) => {
|
|
315
|
+
if (substring.endsWith(")")) {
|
|
316
|
+
prefix = "(";
|
|
317
|
+
suffix = ")";
|
|
318
|
+
}
|
|
319
|
+
let index = argNames.indexOf(name);
|
|
320
|
+
if (index < 0) {
|
|
321
|
+
argNames.push(name);
|
|
322
|
+
index = counter;
|
|
323
|
+
counter += 1;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
index += 1;
|
|
327
|
+
}
|
|
328
|
+
return `$${index}`;
|
|
329
|
+
});
|
|
330
|
+
const descriptions = await asyncDescribeQueries(dbUrl, [
|
|
331
|
+
normalizedStatement,
|
|
332
|
+
]);
|
|
333
|
+
const first = descriptions[0];
|
|
334
|
+
if ("error" in first) {
|
|
335
|
+
report(node, "DBError: " +
|
|
336
|
+
first.error.replace(/\$(\d+)/, (_, d) => `$${prefix}${argNames[parseInt(d, 10) - 1]}${suffix}`));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
description = first.description;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (description == null) {
|
|
344
|
+
throw new Error("Failed to get description of query");
|
|
345
|
+
}
|
|
346
|
+
const err = checkQueryParams(params, description.parameters, argNames, typeChecker);
|
|
347
|
+
if (err != null) {
|
|
348
|
+
report(err.node || node, err.message);
|
|
349
|
+
}
|
|
350
|
+
const isResultCall = ts.isPropertyAccessExpression(parent.expression) &&
|
|
351
|
+
parent.expression.name.text === "result";
|
|
352
|
+
if (isResultCall) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const ppp = parent.parent.parent;
|
|
356
|
+
if (ts.isVariableDeclaration(ppp)) {
|
|
357
|
+
const parentType = typeChecker.getTypeAtLocation(ppp);
|
|
358
|
+
let rowType = parentType;
|
|
359
|
+
if (rowType.isUnion()) {
|
|
360
|
+
const nonNull = rowType.types.filter((t) => !(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)));
|
|
361
|
+
if (nonNull.length === 1)
|
|
362
|
+
rowType = nonNull[0];
|
|
363
|
+
}
|
|
364
|
+
if (typeChecker.isArrayType(rowType)) {
|
|
365
|
+
rowType = typeChecker.getTypeArguments(rowType)[0];
|
|
366
|
+
}
|
|
367
|
+
for (const col of description.columns) {
|
|
368
|
+
const returnedType = pgTypeToTsForColumns(col.type_info, typeChecker);
|
|
369
|
+
const prop = typeChecker.getPropertyOfType(rowType, col.name);
|
|
370
|
+
if (!prop) {
|
|
371
|
+
report(node, `Return type is missing column "${col.name}" which is returned by the query.`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const userDefinedType = typeChecker.getTypeOfSymbol(prop);
|
|
375
|
+
if (!returnedType.some((t) => typeChecker.isTypeAssignableTo(t, userDefinedType))) {
|
|
376
|
+
report(node, `Column "${col.name}" typed as ${typeChecker.typeToString(userDefinedType)} but query returns${returnedType.length > 1 ? " one of" : ""} ${returnedType.map((t) => typeChecker.typeToString(t)).join(", ")}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const expectedNames = new Set(description.columns.map((c) => c.name));
|
|
380
|
+
for (const prop of typeChecker.getPropertiesOfType(rowType)) {
|
|
381
|
+
if (!expectedNames.has(prop.name)) {
|
|
382
|
+
report(node, `Return type has extra property "${prop.name}" not returned by the query`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
console.log(`${count} errors found`);
|
|
388
|
+
process.exit(count > 0 ? 1 : 0);
|
|
389
|
+
}
|
|
390
|
+
main();
|
|
391
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sql-linter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SQL type-checker",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sql-linter": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chalk": "^5.6.2",
|
|
15
|
+
"sql-describe": "^0.1.13",
|
|
16
|
+
"typescript": "5.8.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"tsx": "^4.20.5"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import {
|
|
7
|
+
asyncDescribeQueries,
|
|
8
|
+
type Parameters,
|
|
9
|
+
type PgDescribe,
|
|
10
|
+
type PgType,
|
|
11
|
+
} from "sql-describe";
|
|
12
|
+
import ts from "typescript";
|
|
13
|
+
|
|
14
|
+
const { values, positionals } = parseArgs({
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
options: {
|
|
17
|
+
db: {
|
|
18
|
+
type: "string",
|
|
19
|
+
default: "postgres://postgres:postgres@localhost:5432/postgres",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
allowPositionals: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const dir = positionals[0];
|
|
26
|
+
if (!dir) {
|
|
27
|
+
console.error("Usage: sql-linter <dir> [--db <connection-url>]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const targetDir: string = dir;
|
|
31
|
+
|
|
32
|
+
const dbUrl = values.db!;
|
|
33
|
+
|
|
34
|
+
async function walkInner(dir: string, results: string[]) {
|
|
35
|
+
const files = await fs.readdir(dir);
|
|
36
|
+
|
|
37
|
+
await Promise.all(
|
|
38
|
+
files.map(async (f) => {
|
|
39
|
+
const file = path.resolve(dir, f);
|
|
40
|
+
const stat = await fs.stat(file);
|
|
41
|
+
if (stat?.isDirectory()) {
|
|
42
|
+
await walkInner(file, results);
|
|
43
|
+
} else {
|
|
44
|
+
results.push(file);
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function walk(dir: string): Promise<string[]> {
|
|
53
|
+
const results: string[] = [];
|
|
54
|
+
await walkInner(dir, results);
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function lintNode(
|
|
59
|
+
node: ts.Node,
|
|
60
|
+
callback: (
|
|
61
|
+
taggedTemplateExpression: ts.TaggedTemplateExpression,
|
|
62
|
+
program: ts.Program,
|
|
63
|
+
) => Promise<void>,
|
|
64
|
+
program: ts.Program,
|
|
65
|
+
) {
|
|
66
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
67
|
+
await callback(node, program);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const promises: Promise<void>[] = [];
|
|
71
|
+
ts.forEachChild(node, (n) => {
|
|
72
|
+
promises.push(lintNode(n, callback, program));
|
|
73
|
+
});
|
|
74
|
+
await Promise.all(promises);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function lint(
|
|
78
|
+
dir: string,
|
|
79
|
+
callback: (
|
|
80
|
+
taggedTemplateExpression: ts.TaggedTemplateExpression,
|
|
81
|
+
program: ts.Program,
|
|
82
|
+
) => Promise<void>,
|
|
83
|
+
) {
|
|
84
|
+
const fileNames = await walk(dir);
|
|
85
|
+
const program = ts.createProgram(fileNames, {
|
|
86
|
+
target: ts.ScriptTarget.ES2015,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await Promise.all(
|
|
90
|
+
fileNames.map(async (fileName) => {
|
|
91
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
92
|
+
if (!sourceFile) return;
|
|
93
|
+
await lintNode(sourceFile, callback, program);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pgTypeToTsInnerForParams(
|
|
99
|
+
pg_type: PgType,
|
|
100
|
+
typeChecker: ts.TypeChecker,
|
|
101
|
+
): ts.Type {
|
|
102
|
+
if (typeof pg_type === "string") {
|
|
103
|
+
switch (pg_type) {
|
|
104
|
+
case "Bool":
|
|
105
|
+
return typeChecker.getBooleanType();
|
|
106
|
+
case "Float4":
|
|
107
|
+
case "Float8":
|
|
108
|
+
case "Int2":
|
|
109
|
+
case "Int4":
|
|
110
|
+
return typeChecker.getNumberType();
|
|
111
|
+
case "Int8":
|
|
112
|
+
case "Numeric":
|
|
113
|
+
return (typeChecker as any).getUnionType([
|
|
114
|
+
typeChecker.getNumberType(),
|
|
115
|
+
typeChecker.getBigIntType(),
|
|
116
|
+
]);
|
|
117
|
+
case "Interval":
|
|
118
|
+
case "Text":
|
|
119
|
+
case "Varchar":
|
|
120
|
+
case "Time":
|
|
121
|
+
case "Macaddr":
|
|
122
|
+
case "Uuid":
|
|
123
|
+
return typeChecker.getStringType();
|
|
124
|
+
case "Date":
|
|
125
|
+
case "Timestamp":
|
|
126
|
+
case "Timestamptz": {
|
|
127
|
+
const dateSymbol = (typeChecker as any).resolveName(
|
|
128
|
+
"Date",
|
|
129
|
+
undefined,
|
|
130
|
+
ts.SymbolFlags.Type,
|
|
131
|
+
false,
|
|
132
|
+
);
|
|
133
|
+
const dateType = typeChecker.getDeclaredTypeOfSymbol(dateSymbol);
|
|
134
|
+
return (typeChecker as any).getUnionType([
|
|
135
|
+
typeChecker.getStringType(),
|
|
136
|
+
dateType,
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
case "TextArray":
|
|
140
|
+
return (typeChecker as any).createArrayType(
|
|
141
|
+
typeChecker.getStringType(),
|
|
142
|
+
);
|
|
143
|
+
case "Float4Array":
|
|
144
|
+
case "Float8Array":
|
|
145
|
+
case "Int2Array":
|
|
146
|
+
case "Int4Array":
|
|
147
|
+
case "Int8Array":
|
|
148
|
+
return (typeChecker as any).createArrayType(
|
|
149
|
+
(typeChecker as any).getUnionType([
|
|
150
|
+
typeChecker.getNumberType(),
|
|
151
|
+
(typeChecker as any).createArrayType(typeChecker.getNumberType()),
|
|
152
|
+
]),
|
|
153
|
+
);
|
|
154
|
+
case "Jsonb":
|
|
155
|
+
return typeChecker.getAnyType();
|
|
156
|
+
}
|
|
157
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
158
|
+
}
|
|
159
|
+
if ("Custom" in pg_type) {
|
|
160
|
+
if (typeof pg_type.Custom.kind !== "string") {
|
|
161
|
+
if ("Enum" in pg_type.Custom.kind) {
|
|
162
|
+
return typeChecker.getStringType();
|
|
163
|
+
} else if ("Array" in pg_type.Custom.kind) {
|
|
164
|
+
return (typeChecker as any).createArrayType(
|
|
165
|
+
pgTypeToTsInnerForParams(pg_type.Custom.kind.Array, typeChecker),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pgTypeToTsMappingForParams(
|
|
174
|
+
pg_type: PgType,
|
|
175
|
+
nullable: boolean | null | undefined,
|
|
176
|
+
typeChecker: ts.TypeChecker,
|
|
177
|
+
): ts.Type {
|
|
178
|
+
const inner = pgTypeToTsInnerForParams(pg_type, typeChecker);
|
|
179
|
+
if (nullable === true) {
|
|
180
|
+
return typeChecker.getNullableType(inner, ts.TypeFlags.Null);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return inner;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function pgTypeToTsForColumns(
|
|
187
|
+
pg_type: PgType,
|
|
188
|
+
typeChecker: ts.TypeChecker,
|
|
189
|
+
): ts.Type[] {
|
|
190
|
+
if (typeof pg_type === "string") {
|
|
191
|
+
switch (pg_type) {
|
|
192
|
+
case "Bool":
|
|
193
|
+
return [typeChecker.getBooleanType()];
|
|
194
|
+
case "Float4":
|
|
195
|
+
case "Float8":
|
|
196
|
+
case "Int2":
|
|
197
|
+
case "Int4":
|
|
198
|
+
return [typeChecker.getNumberType()];
|
|
199
|
+
case "Int8":
|
|
200
|
+
case "Numeric":
|
|
201
|
+
return [typeChecker.getStringType()];
|
|
202
|
+
case "TstzRange":
|
|
203
|
+
case "Uuid":
|
|
204
|
+
case "Macaddr":
|
|
205
|
+
case "Text":
|
|
206
|
+
case "Varchar":
|
|
207
|
+
return [typeChecker.getStringType()];
|
|
208
|
+
case "Date":
|
|
209
|
+
case "Timestamp":
|
|
210
|
+
case "Timestamptz": {
|
|
211
|
+
const dateSymbol = (typeChecker as any).resolveName(
|
|
212
|
+
"Date",
|
|
213
|
+
undefined,
|
|
214
|
+
ts.SymbolFlags.Type,
|
|
215
|
+
false,
|
|
216
|
+
);
|
|
217
|
+
const dateType = typeChecker.getDeclaredTypeOfSymbol(dateSymbol);
|
|
218
|
+
return [dateType];
|
|
219
|
+
}
|
|
220
|
+
case "TextArray":
|
|
221
|
+
return [
|
|
222
|
+
(typeChecker as any).createArrayType(typeChecker.getStringType()),
|
|
223
|
+
];
|
|
224
|
+
case "Float4Array":
|
|
225
|
+
case "Float8Array":
|
|
226
|
+
case "Int2Array":
|
|
227
|
+
case "Int4Array":
|
|
228
|
+
case "Int8Array":
|
|
229
|
+
return [
|
|
230
|
+
(typeChecker as any).createArrayType(typeChecker.getNumberType()),
|
|
231
|
+
(typeChecker as any).createArrayType(
|
|
232
|
+
(typeChecker as any).createArrayType(typeChecker.getNumberType()),
|
|
233
|
+
),
|
|
234
|
+
];
|
|
235
|
+
case "JsonArray":
|
|
236
|
+
case "JsonbArray":
|
|
237
|
+
case "Json":
|
|
238
|
+
case "Jsonb":
|
|
239
|
+
return [typeChecker.getAnyType()];
|
|
240
|
+
}
|
|
241
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
242
|
+
}
|
|
243
|
+
if ("Custom" in pg_type) {
|
|
244
|
+
if (typeof pg_type.Custom.kind !== "string") {
|
|
245
|
+
if ("Enum" in pg_type.Custom.kind) {
|
|
246
|
+
return [
|
|
247
|
+
(typeChecker as any).getUnionType(
|
|
248
|
+
pg_type.Custom.kind.Enum.map((v) =>
|
|
249
|
+
typeChecker.getStringLiteralType(v),
|
|
250
|
+
),
|
|
251
|
+
),
|
|
252
|
+
];
|
|
253
|
+
} else if ("Array" in pg_type.Custom.kind) {
|
|
254
|
+
return [
|
|
255
|
+
(typeChecker as any).createArrayType(
|
|
256
|
+
pgTypeToTsInnerForParams(pg_type.Custom.kind.Array, typeChecker),
|
|
257
|
+
),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw `Unsupported data type ${JSON.stringify(pg_type)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type CheckQueryParamsError = {
|
|
266
|
+
node?: ts.Node;
|
|
267
|
+
message: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
function checkQueryParams(
|
|
271
|
+
givenParams: ts.Expression | undefined,
|
|
272
|
+
expectedParams: Parameters | null,
|
|
273
|
+
argNames: string[] | null,
|
|
274
|
+
typeChecker: ts.TypeChecker,
|
|
275
|
+
): CheckQueryParamsError | null {
|
|
276
|
+
if (expectedParams == null) {
|
|
277
|
+
if (givenParams != null) {
|
|
278
|
+
return {
|
|
279
|
+
node: givenParams,
|
|
280
|
+
message: "This query takes no params, but you supplied some here",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if ("Right" in expectedParams) {
|
|
287
|
+
throw new Error("This should only happen for mssql");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (givenParams == null) {
|
|
291
|
+
if (expectedParams == null || expectedParams.Left.length === 0) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return { message: "This query expects parameters, but none were given" };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const expectedParameters = expectedParams.Left;
|
|
298
|
+
|
|
299
|
+
const givenParameters: Record<string, ts.Type> = {};
|
|
300
|
+
|
|
301
|
+
if (argNames != null) {
|
|
302
|
+
const t = typeChecker.getTypeAtLocation(givenParams);
|
|
303
|
+
for (const property of t.getProperties()) {
|
|
304
|
+
const t = typeChecker.getTypeOfSymbol(property);
|
|
305
|
+
givenParameters[property.escapedName.toString()] = t;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
if (!ts.isArrayLiteralExpression(givenParams)) {
|
|
309
|
+
return {
|
|
310
|
+
node: givenParams,
|
|
311
|
+
message:
|
|
312
|
+
"The arguments to the call should be an array e.g.: `[value1, value2]`",
|
|
313
|
+
};
|
|
314
|
+
} else {
|
|
315
|
+
givenParams.elements.forEach((e, i) => {
|
|
316
|
+
if (e == null) return;
|
|
317
|
+
const t = typeChecker.getTypeAtLocation(e);
|
|
318
|
+
givenParameters[i] = t;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let i = 0;
|
|
324
|
+
for (const expectedParam of expectedParameters) {
|
|
325
|
+
const name = argNames?.[i] || i;
|
|
326
|
+
const eName = argNames?.[i] || i + 1;
|
|
327
|
+
const paramType = givenParameters[name]!;
|
|
328
|
+
try {
|
|
329
|
+
const expected = pgTypeToTsMappingForParams(
|
|
330
|
+
expectedParam,
|
|
331
|
+
false,
|
|
332
|
+
typeChecker,
|
|
333
|
+
);
|
|
334
|
+
if (
|
|
335
|
+
!typeChecker.isTypeAssignableTo(
|
|
336
|
+
paramType,
|
|
337
|
+
typeChecker.getNullableType(expected, ts.TypeFlags.Undefined),
|
|
338
|
+
)
|
|
339
|
+
) {
|
|
340
|
+
return {
|
|
341
|
+
message: `Expected a ${typeChecker.typeToString(expected)} for $${eName}, found a ${typeChecker.typeToString(paramType)}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return {
|
|
346
|
+
message: e as string,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
i += 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let count = 0;
|
|
356
|
+
|
|
357
|
+
function report(node: ts.Node, message: string) {
|
|
358
|
+
count++;
|
|
359
|
+
const sourceFile = node.getSourceFile();
|
|
360
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
361
|
+
node.getStart(),
|
|
362
|
+
);
|
|
363
|
+
console.log(`${sourceFile.fileName}:${line}:${character}:
|
|
364
|
+
${chalk.red(message)}
|
|
365
|
+
`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function main() {
|
|
369
|
+
await lint(targetDir, async (node, program) => {
|
|
370
|
+
let typeChecker = program.getTypeChecker();
|
|
371
|
+
|
|
372
|
+
if (!ts.isIdentifier(node.tag) || node.tag.escapedText !== "sql") return;
|
|
373
|
+
let sqlStatement: string = (node.template as any).rawText;
|
|
374
|
+
if (sqlStatement == null || sqlStatement.length === 0) {
|
|
375
|
+
report(node, "Empty sql query");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const parent = node.parent;
|
|
379
|
+
if (!ts.isCallExpression(parent)) {
|
|
380
|
+
report(
|
|
381
|
+
parent,
|
|
382
|
+
"The parent of a sql`...` call should be a Call expression.",
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (parent.arguments.length > 2) {
|
|
387
|
+
report(parent, "Expected at most 2 arguments to this call");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const params = parent.arguments[1];
|
|
392
|
+
|
|
393
|
+
const usesRemappings =
|
|
394
|
+
sqlStatement.includes("$/") || sqlStatement.includes("$(");
|
|
395
|
+
let description: PgDescribe;
|
|
396
|
+
let argNames: string[] | null = null;
|
|
397
|
+
sqlStatement = sqlStatement
|
|
398
|
+
.replaceAll(":csv", "")
|
|
399
|
+
.replaceAll(/[^:]:json/g, "");
|
|
400
|
+
|
|
401
|
+
if (!usesRemappings) {
|
|
402
|
+
const descriptions = await asyncDescribeQueries(dbUrl, [sqlStatement]);
|
|
403
|
+
const first = descriptions[0]!;
|
|
404
|
+
if ("error" in first) {
|
|
405
|
+
report(node, `DBError: ${first.error}`);
|
|
406
|
+
return;
|
|
407
|
+
} else {
|
|
408
|
+
description = first.description;
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
argNames = [];
|
|
412
|
+
let prefix = "/";
|
|
413
|
+
let suffix = "/";
|
|
414
|
+
let counter = 1;
|
|
415
|
+
const normalizedStatement = sqlStatement.replaceAll(
|
|
416
|
+
/\$[\/\(]\s*([a-zA-Z0-9_]*)\s*[\/\)]/g,
|
|
417
|
+
(substring, name) => {
|
|
418
|
+
if (substring.endsWith(")")) {
|
|
419
|
+
prefix = "(";
|
|
420
|
+
suffix = ")";
|
|
421
|
+
}
|
|
422
|
+
let index = argNames!.indexOf(name);
|
|
423
|
+
if (index < 0) {
|
|
424
|
+
argNames!.push(name);
|
|
425
|
+
index = counter;
|
|
426
|
+
counter += 1;
|
|
427
|
+
} else {
|
|
428
|
+
index += 1;
|
|
429
|
+
}
|
|
430
|
+
return `$${index}`;
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
const descriptions = await asyncDescribeQueries(dbUrl, [
|
|
434
|
+
normalizedStatement,
|
|
435
|
+
]);
|
|
436
|
+
const first = descriptions[0]!;
|
|
437
|
+
if ("error" in first) {
|
|
438
|
+
report(
|
|
439
|
+
node,
|
|
440
|
+
"DBError: " +
|
|
441
|
+
first.error.replace(
|
|
442
|
+
/\$(\d+)/,
|
|
443
|
+
(_, d) => `$${prefix}${argNames![parseInt(d, 10) - 1]}${suffix}`,
|
|
444
|
+
),
|
|
445
|
+
);
|
|
446
|
+
return;
|
|
447
|
+
} else {
|
|
448
|
+
description = first.description;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (description == null) {
|
|
453
|
+
throw new Error("Failed to get description of query");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const err = checkQueryParams(
|
|
457
|
+
params,
|
|
458
|
+
description.parameters,
|
|
459
|
+
argNames,
|
|
460
|
+
typeChecker,
|
|
461
|
+
);
|
|
462
|
+
if (err != null) {
|
|
463
|
+
report(err.node || node, err.message);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const isResultCall =
|
|
467
|
+
ts.isPropertyAccessExpression(parent.expression) &&
|
|
468
|
+
parent.expression.name.text === "result";
|
|
469
|
+
if (isResultCall) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const ppp = parent.parent.parent;
|
|
474
|
+
if (ts.isVariableDeclaration(ppp)) {
|
|
475
|
+
const parentType = typeChecker.getTypeAtLocation(ppp);
|
|
476
|
+
|
|
477
|
+
let rowType = parentType;
|
|
478
|
+
if (rowType.isUnion()) {
|
|
479
|
+
const nonNull = rowType.types.filter(
|
|
480
|
+
(t) => !(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)),
|
|
481
|
+
);
|
|
482
|
+
if (nonNull.length === 1) rowType = nonNull[0]!;
|
|
483
|
+
}
|
|
484
|
+
if ((typeChecker as any).isArrayType(rowType)) {
|
|
485
|
+
rowType = typeChecker.getTypeArguments(rowType as ts.TypeReference)[0]!;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
for (const col of description.columns) {
|
|
489
|
+
const returnedType = pgTypeToTsForColumns(col.type_info, typeChecker);
|
|
490
|
+
const prop = typeChecker.getPropertyOfType(rowType, col.name);
|
|
491
|
+
if (!prop) {
|
|
492
|
+
report(
|
|
493
|
+
node,
|
|
494
|
+
`Return type is missing column "${col.name}" which is returned by the query.`,
|
|
495
|
+
);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const userDefinedType = typeChecker.getTypeOfSymbol(prop);
|
|
499
|
+
if (
|
|
500
|
+
!returnedType.some((t) =>
|
|
501
|
+
typeChecker.isTypeAssignableTo(t, userDefinedType),
|
|
502
|
+
)
|
|
503
|
+
) {
|
|
504
|
+
report(
|
|
505
|
+
node,
|
|
506
|
+
`Column "${col.name}" typed as ${typeChecker.typeToString(userDefinedType)} but query returns${returnedType.length > 1 ? " one of" : ""} ${returnedType.map((t) => typeChecker.typeToString(t)).join(", ")}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const expectedNames = new Set(description.columns.map((c) => c.name));
|
|
512
|
+
for (const prop of typeChecker.getPropertiesOfType(rowType)) {
|
|
513
|
+
if (!expectedNames.has(prop.name)) {
|
|
514
|
+
report(
|
|
515
|
+
node,
|
|
516
|
+
`Return type has extra property "${prop.name}" not returned by the query`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
console.log(`${count} errors found`);
|
|
524
|
+
process.exit(count > 0 ? 1 : 0);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
main();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|