@webergency-utils/typechecker 0.1.7 → 0.1.9
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/README.md +21 -0
- package/dist/engine/generators.js +1 -17
- package/dist/engine/resolver.js +40 -24
- package/dist/engine/staticAsserts.d.ts +34 -0
- package/dist/engine/staticAsserts.js +317 -0
- package/dist/runtime/tags/constraint.d.ts +15 -15
- package/dist/runtime/tags/tag.d.ts +1 -1
- package/dist/runtime/tags/transform.d.ts +8 -8
- package/dist/runtime/tags.d.ts +3 -6
- package/dist/transformer.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -327,6 +327,27 @@ Define defaults for optional fields:
|
|
|
327
327
|
|
|
328
328
|
- `tag.Default<Value>`: Injects the specified `Value` if the property is `undefined` at validation time.
|
|
329
329
|
|
|
330
|
+
### Assignability & compile-time constants
|
|
331
|
+
|
|
332
|
+
Constraint tags use **optional** phantom properties (same pattern as `tag.Default`), so plain values assign to tagged types:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const age: number & Minimum<18> = 18; // OK
|
|
336
|
+
const plain: { age: number } = { age: 5 };
|
|
337
|
+
const tagged: { age: number & Minimum<18> } = plain; // OK
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
When the TypeScript plugin is enabled, **compile-time constants** that violate constraints produce diagnostics:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const tooYoung: number & Minimum<18> = 5; // Error: does not satisfy Minimum<18>
|
|
344
|
+
const short: string & MinLength<3> = 'ab'; // Error: does not satisfy MinLength<3>
|
|
345
|
+
const empty: string[] & MinItems<1> = []; // Error: does not satisfy MinItems<1>
|
|
346
|
+
const dupes: number[] & UniqueItems = [1, 1]; // Error: does not satisfy UniqueItems
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Non-constants (`number`, variables, function results) are not checked statically — use `is` / `assert` / `validate` at runtime.
|
|
350
|
+
|
|
330
351
|
#### Example:
|
|
331
352
|
```typescript
|
|
332
353
|
import { MinLength, Minimum, Format, UniqueItems, tag, transform, constraint } from '@webergency-utils/typechecker';
|
|
@@ -199,23 +199,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object') {
|
|
|
199
199
|
const tpl = `
|
|
200
200
|
(v, path, ctx) => {
|
|
201
201
|
if (!validators.object(v, path, ctx, __KEYS__, __EXPECTED__)) return v;
|
|
202
|
-
|
|
203
|
-
if (ctx.mode === 'strip') {
|
|
204
|
-
let hasAdditional = false;
|
|
205
|
-
const keys = Object.keys(v);
|
|
206
|
-
const allowed = __KEYS__;
|
|
207
|
-
if (keys.length > allowed.length) {
|
|
208
|
-
hasAdditional = true;
|
|
209
|
-
} else {
|
|
210
|
-
for (let i = 0; i < keys.length; i++) {
|
|
211
|
-
if (!allowed.includes(keys[i])) {
|
|
212
|
-
hasAdditional = true;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (hasAdditional) data = {};
|
|
218
|
-
}
|
|
202
|
+
const data = ctx.mode === 'strip' ? {} : v;
|
|
219
203
|
validators.props(v, data, path, ctx, __PROPS__);
|
|
220
204
|
return data;
|
|
221
205
|
}
|
package/dist/engine/resolver.js
CHANGED
|
@@ -13,6 +13,32 @@ function getStringLiteralValue(type) {
|
|
|
13
13
|
}
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
|
+
/** Optional tag phantoms are `V | undefined` — strip undefined before reading literals/symbols. */
|
|
17
|
+
function stripUndefinedFromType(type) {
|
|
18
|
+
if (!type.isUnion()) {
|
|
19
|
+
return type;
|
|
20
|
+
}
|
|
21
|
+
const nonUndefined = type.types.filter(t => !(t.getFlags() & ts.TypeFlags.Undefined));
|
|
22
|
+
if (nonUndefined.length === 1) {
|
|
23
|
+
return nonUndefined[0];
|
|
24
|
+
}
|
|
25
|
+
if (nonUndefined.length > 1) {
|
|
26
|
+
// Keep union of remaining members (e.g. string literal | other)
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
return type;
|
|
30
|
+
}
|
|
31
|
+
function getTagPropertyValue(type) {
|
|
32
|
+
const actual = stripUndefinedFromType(type);
|
|
33
|
+
let val = actual.value;
|
|
34
|
+
if (val === undefined && (actual.getFlags() & ts.TypeFlags.BooleanLiteral)) {
|
|
35
|
+
val = actual.intrinsicName === 'true';
|
|
36
|
+
}
|
|
37
|
+
if (val === undefined && (actual.getFlags() & ts.TypeFlags.Null)) {
|
|
38
|
+
val = null;
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
}
|
|
16
42
|
function minifyTypeString(str) {
|
|
17
43
|
return str
|
|
18
44
|
.replace(/\{\s+/g, '{')
|
|
@@ -71,13 +97,8 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
71
97
|
const pName = prop.getName();
|
|
72
98
|
if (pName.startsWith('__')) {
|
|
73
99
|
const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
val = pType.intrinsicName === 'true';
|
|
77
|
-
}
|
|
78
|
-
if (val === undefined && (pType.getFlags() & ts.TypeFlags.Null)) {
|
|
79
|
-
val = null;
|
|
80
|
-
}
|
|
100
|
+
const actualType = stripUndefinedFromType(pType);
|
|
101
|
+
const val = getTagPropertyValue(pType);
|
|
81
102
|
if (pName === '__default') {
|
|
82
103
|
constraints.push({ type: 'default', value: val });
|
|
83
104
|
}
|
|
@@ -108,7 +129,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
108
129
|
else if (pName === '__transform_custom') {
|
|
109
130
|
let fnName;
|
|
110
131
|
let filePath;
|
|
111
|
-
const symbol = pType.getSymbol() || pType.aliasSymbol;
|
|
132
|
+
const symbol = actualType.getSymbol() || actualType.aliasSymbol || pType.getSymbol() || pType.aliasSymbol;
|
|
112
133
|
if (symbol) {
|
|
113
134
|
fnName = symbol.getName();
|
|
114
135
|
let dec = symbol.valueDeclaration || symbol.declarations?.[0];
|
|
@@ -131,7 +152,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
else {
|
|
134
|
-
const str = checker.typeToString(
|
|
155
|
+
const str = checker.typeToString(actualType);
|
|
135
156
|
const match = str.match(/typeof\s+([a-zA-Z0-9_]+)/);
|
|
136
157
|
if (match) {
|
|
137
158
|
fnName = match[1];
|
|
@@ -148,7 +169,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
148
169
|
else if (pName === '__custom') {
|
|
149
170
|
let fnName;
|
|
150
171
|
let filePath;
|
|
151
|
-
const symbol = pType.getSymbol() || pType.aliasSymbol;
|
|
172
|
+
const symbol = actualType.getSymbol() || actualType.aliasSymbol || pType.getSymbol() || pType.aliasSymbol;
|
|
152
173
|
if (symbol) {
|
|
153
174
|
fnName = symbol.getName();
|
|
154
175
|
let dec = symbol.valueDeclaration || symbol.declarations?.[0];
|
|
@@ -171,7 +192,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
else {
|
|
174
|
-
const str = checker.typeToString(
|
|
195
|
+
const str = checker.typeToString(actualType);
|
|
175
196
|
const match = str.match(/typeof\s+([a-zA-Z0-9_]+)/);
|
|
176
197
|
if (match) {
|
|
177
198
|
fnName = match[1];
|
|
@@ -237,11 +258,11 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
237
258
|
}
|
|
238
259
|
else if (pName === '__requires') {
|
|
239
260
|
let reqVal;
|
|
240
|
-
if (
|
|
241
|
-
reqVal =
|
|
261
|
+
if (actualType.isStringLiteral()) {
|
|
262
|
+
reqVal = actualType.value;
|
|
242
263
|
}
|
|
243
264
|
else {
|
|
244
|
-
const typeArgs =
|
|
265
|
+
const typeArgs = actualType.typeArguments || [];
|
|
245
266
|
const items = [];
|
|
246
267
|
for (const arg of typeArgs) {
|
|
247
268
|
if (arg.isStringLiteral()) {
|
|
@@ -695,13 +716,8 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
695
716
|
const pName = prop.getName();
|
|
696
717
|
if (pName.startsWith('__')) {
|
|
697
718
|
const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
val = pType.intrinsicName === 'true';
|
|
701
|
-
}
|
|
702
|
-
if (val === undefined && (pType.getFlags() & ts.TypeFlags.Null)) {
|
|
703
|
-
val = null;
|
|
704
|
-
}
|
|
719
|
+
const actualType = stripUndefinedFromType(pType);
|
|
720
|
+
const val = getTagPropertyValue(pType);
|
|
705
721
|
if (pName === '__default') {
|
|
706
722
|
constraints.default = val;
|
|
707
723
|
}
|
|
@@ -743,11 +759,11 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
743
759
|
}
|
|
744
760
|
else if (pName === '__requires') {
|
|
745
761
|
let reqVal;
|
|
746
|
-
if (
|
|
747
|
-
reqVal =
|
|
762
|
+
if (actualType.isStringLiteral()) {
|
|
763
|
+
reqVal = actualType.value;
|
|
748
764
|
}
|
|
749
765
|
else {
|
|
750
|
-
const typeArgs =
|
|
766
|
+
const typeArgs = actualType.typeArguments || [];
|
|
751
767
|
const items = [];
|
|
752
768
|
for (const arg of typeArgs) {
|
|
753
769
|
if (arg.isStringLiteral()) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export interface IStaticConstraint {
|
|
3
|
+
type: string;
|
|
4
|
+
value: any;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract Minimum / Length / Items-style constraints from a (possibly intersected) type.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractStaticConstraints(type: ts.Type, checker: ts.TypeChecker): IStaticConstraint[];
|
|
11
|
+
type ConstantValue = {
|
|
12
|
+
kind: 'number';
|
|
13
|
+
value: number | bigint;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'string';
|
|
16
|
+
value: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'array';
|
|
19
|
+
value: any[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a compile-time constant from an expression, or undefined if not constant.
|
|
23
|
+
*/
|
|
24
|
+
export declare function tryGetConstantValue(expr: ts.Expression): ConstantValue | undefined;
|
|
25
|
+
export declare function evaluateStaticConstraints(constant: ConstantValue, constraints: IStaticConstraint[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Walk a source file and emit diagnostics for constant values that violate tag constraints.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectStaticConstraintDiagnostics(sourceFile: ts.SourceFile, checker: ts.TypeChecker): ts.Diagnostic[];
|
|
30
|
+
/**
|
|
31
|
+
* Analyze the program and patch getSemanticDiagnostics so constant constraint violations appear in tsc/IDE.
|
|
32
|
+
*/
|
|
33
|
+
export declare function installStaticConstraintDiagnostics(program: ts.Program): ts.Diagnostic[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const CONSTRAINT_KEYS = {
|
|
3
|
+
__minLength: 'minLength',
|
|
4
|
+
__maxLength: 'maxLength',
|
|
5
|
+
__minimum: 'minimum',
|
|
6
|
+
__maximum: 'maximum',
|
|
7
|
+
__exclusiveMinimum: 'exclusiveMinimum',
|
|
8
|
+
__exclusiveMaximum: 'exclusiveMaximum',
|
|
9
|
+
__multipleOf: 'multipleOf',
|
|
10
|
+
__minItems: 'minItems',
|
|
11
|
+
__maxItems: 'maxItems',
|
|
12
|
+
__uniqueItems: 'uniqueItems'
|
|
13
|
+
};
|
|
14
|
+
function stripUndefined(type) {
|
|
15
|
+
if (!type.isUnion()) {
|
|
16
|
+
return type;
|
|
17
|
+
}
|
|
18
|
+
const nonUndefined = type.types.filter(t => !(t.getFlags() & ts.TypeFlags.Undefined));
|
|
19
|
+
if (nonUndefined.length === 1) {
|
|
20
|
+
return nonUndefined[0];
|
|
21
|
+
}
|
|
22
|
+
return type;
|
|
23
|
+
}
|
|
24
|
+
function getLiteralValue(type) {
|
|
25
|
+
const actual = stripUndefined(type);
|
|
26
|
+
if (actual.value !== undefined) {
|
|
27
|
+
return actual.value;
|
|
28
|
+
}
|
|
29
|
+
if (actual.getFlags() & ts.TypeFlags.BooleanLiteral) {
|
|
30
|
+
return actual.intrinsicName === 'true';
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function getStringLiteralValue(type) {
|
|
35
|
+
const actual = stripUndefined(type);
|
|
36
|
+
if (actual.isStringLiteral()) {
|
|
37
|
+
return actual.value;
|
|
38
|
+
}
|
|
39
|
+
if (actual.isUnion()) {
|
|
40
|
+
const literal = actual.types.find(t => t.isStringLiteral());
|
|
41
|
+
if (literal && literal.isStringLiteral()) {
|
|
42
|
+
return literal.value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function getSymbolType(checker, symbol) {
|
|
48
|
+
const decl = symbol.valueDeclaration || symbol.declarations?.[0];
|
|
49
|
+
if (!decl) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return checker.getTypeOfSymbolAtLocation(symbol, decl);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract Minimum / Length / Items-style constraints from a (possibly intersected) type.
|
|
56
|
+
*/
|
|
57
|
+
export function extractStaticConstraints(type, checker) {
|
|
58
|
+
const constraints = [];
|
|
59
|
+
const types = type.isIntersection() ? type.types : [type];
|
|
60
|
+
for (const sub of types) {
|
|
61
|
+
const props = checker.getPropertiesOfType(sub);
|
|
62
|
+
for (const prop of props) {
|
|
63
|
+
const pName = prop.getName();
|
|
64
|
+
const mapped = CONSTRAINT_KEYS[pName];
|
|
65
|
+
if (!mapped) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const pType = getSymbolType(checker, prop);
|
|
69
|
+
if (!pType) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let val = getLiteralValue(pType);
|
|
73
|
+
if (mapped === 'uniqueItems') {
|
|
74
|
+
val = true;
|
|
75
|
+
}
|
|
76
|
+
if (val === undefined) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const msgProp = props.find(p => p.getName() === `${pName}_message`);
|
|
80
|
+
let message;
|
|
81
|
+
if (msgProp) {
|
|
82
|
+
const msgType = getSymbolType(checker, msgProp);
|
|
83
|
+
if (msgType) {
|
|
84
|
+
message = getStringLiteralValue(msgType);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
constraints.push({ type: mapped, value: val, message });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return constraints;
|
|
91
|
+
}
|
|
92
|
+
function unwrapExpression(expr) {
|
|
93
|
+
while (ts.isParenthesizedExpression(expr) ||
|
|
94
|
+
ts.isAsExpression(expr) ||
|
|
95
|
+
ts.isTypeAssertionExpression(expr) ||
|
|
96
|
+
ts.isSatisfiesExpression(expr)) {
|
|
97
|
+
expr = expr.expression;
|
|
98
|
+
}
|
|
99
|
+
return expr;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a compile-time constant from an expression, or undefined if not constant.
|
|
103
|
+
*/
|
|
104
|
+
export function tryGetConstantValue(expr) {
|
|
105
|
+
expr = unwrapExpression(expr);
|
|
106
|
+
if (ts.isNumericLiteral(expr)) {
|
|
107
|
+
return { kind: 'number', value: Number(expr.text) };
|
|
108
|
+
}
|
|
109
|
+
if (ts.isBigIntLiteral(expr)) {
|
|
110
|
+
return { kind: 'number', value: BigInt(expr.text.slice(0, -1)) };
|
|
111
|
+
}
|
|
112
|
+
if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.MinusToken) {
|
|
113
|
+
const inner = tryGetConstantValue(expr.operand);
|
|
114
|
+
if (inner?.kind === 'number') {
|
|
115
|
+
if (typeof inner.value === 'bigint') {
|
|
116
|
+
return { kind: 'number', value: -inner.value };
|
|
117
|
+
}
|
|
118
|
+
return { kind: 'number', value: -inner.value };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
122
|
+
return { kind: 'string', value: expr.text };
|
|
123
|
+
}
|
|
124
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
125
|
+
const items = [];
|
|
126
|
+
for (const el of expr.elements) {
|
|
127
|
+
if (ts.isSpreadElement(el)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const c = tryGetConstantValue(el);
|
|
131
|
+
if (!c) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
if (c.kind === 'array') {
|
|
135
|
+
items.push(c.value);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
items.push(c.value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'array', value: items };
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
export function evaluateStaticConstraints(constant, constraints) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
for (const c of constraints) {
|
|
148
|
+
if (c.type === 'minimum' && constant.kind === 'number') {
|
|
149
|
+
if (constant.value < c.value) {
|
|
150
|
+
errors.push(c.message || `Value ${String(constant.value)} does not satisfy Minimum<${c.value}>`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (c.type === 'maximum' && constant.kind === 'number') {
|
|
154
|
+
if (constant.value > c.value) {
|
|
155
|
+
errors.push(c.message || `Value ${String(constant.value)} does not satisfy Maximum<${c.value}>`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (c.type === 'exclusiveMinimum' && constant.kind === 'number') {
|
|
159
|
+
if (constant.value <= c.value) {
|
|
160
|
+
errors.push(c.message || `Value ${String(constant.value)} does not satisfy ExclusiveMinimum<${c.value}>`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (c.type === 'exclusiveMaximum' && constant.kind === 'number') {
|
|
164
|
+
if (constant.value >= c.value) {
|
|
165
|
+
errors.push(c.message || `Value ${String(constant.value)} does not satisfy ExclusiveMaximum<${c.value}>`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (c.type === 'multipleOf' && constant.kind === 'number') {
|
|
169
|
+
const v = constant.value;
|
|
170
|
+
const n = c.value;
|
|
171
|
+
let ok = true;
|
|
172
|
+
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
173
|
+
ok = BigInt(v) % BigInt(n) === 0n;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
ok = v % n === 0;
|
|
177
|
+
}
|
|
178
|
+
if (!ok) {
|
|
179
|
+
errors.push(c.message || `Value ${String(v)} does not satisfy MultipleOf<${n}>`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (c.type === 'minLength' && constant.kind === 'string') {
|
|
183
|
+
if (constant.value.length < c.value) {
|
|
184
|
+
errors.push(c.message || `Value length ${constant.value.length} does not satisfy MinLength<${c.value}>`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (c.type === 'maxLength' && constant.kind === 'string') {
|
|
188
|
+
if (constant.value.length > c.value) {
|
|
189
|
+
errors.push(c.message || `Value length ${constant.value.length} does not satisfy MaxLength<${c.value}>`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (c.type === 'minItems' && constant.kind === 'array') {
|
|
193
|
+
if (constant.value.length < c.value) {
|
|
194
|
+
errors.push(c.message || `Array length ${constant.value.length} does not satisfy MinItems<${c.value}>`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (c.type === 'maxItems' && constant.kind === 'array') {
|
|
198
|
+
if (constant.value.length > c.value) {
|
|
199
|
+
errors.push(c.message || `Array length ${constant.value.length} does not satisfy MaxItems<${c.value}>`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (c.type === 'uniqueItems' && constant.kind === 'array') {
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
for (const item of constant.value) {
|
|
205
|
+
const key = typeof item === 'object' && item !== null ? JSON.stringify(item) : item;
|
|
206
|
+
if (seen.has(key)) {
|
|
207
|
+
errors.push(c.message || 'Array does not satisfy UniqueItems');
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
seen.add(key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
function createDiagnostic(node, message) {
|
|
217
|
+
const start = node.getStart();
|
|
218
|
+
const length = node.getWidth();
|
|
219
|
+
return {
|
|
220
|
+
file: node.getSourceFile(),
|
|
221
|
+
start,
|
|
222
|
+
length,
|
|
223
|
+
messageText: message,
|
|
224
|
+
category: ts.DiagnosticCategory.Error,
|
|
225
|
+
code: 90001,
|
|
226
|
+
source: 'webergency-typechecker'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function checkExpressionAgainstType(expr, type, checker, diagnostics) {
|
|
230
|
+
const constraints = extractStaticConstraints(type, checker);
|
|
231
|
+
if (constraints.length === 0) {
|
|
232
|
+
// Still recurse into object literals for nested tagged properties
|
|
233
|
+
if (ts.isObjectLiteralExpression(unwrapExpression(expr))) {
|
|
234
|
+
checkObjectLiteral(unwrapExpression(expr), type, checker, diagnostics);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const constant = tryGetConstantValue(expr);
|
|
239
|
+
if (!constant) {
|
|
240
|
+
if (ts.isObjectLiteralExpression(unwrapExpression(expr))) {
|
|
241
|
+
checkObjectLiteral(unwrapExpression(expr), type, checker, diagnostics);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const msg of evaluateStaticConstraints(constant, constraints)) {
|
|
246
|
+
diagnostics.push(createDiagnostic(expr, msg));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function checkObjectLiteral(obj, type, checker, diagnostics) {
|
|
250
|
+
for (const prop of obj.properties) {
|
|
251
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const name = prop.name;
|
|
255
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const propName = name.text;
|
|
259
|
+
const symbol = checker.getPropertyOfType(type, propName);
|
|
260
|
+
if (!symbol) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const propType = getSymbolType(checker, symbol);
|
|
264
|
+
if (!propType) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
checkExpressionAgainstType(prop.initializer, propType, checker, diagnostics);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Walk a source file and emit diagnostics for constant values that violate tag constraints.
|
|
272
|
+
*/
|
|
273
|
+
export function collectStaticConstraintDiagnostics(sourceFile, checker) {
|
|
274
|
+
const diagnostics = [];
|
|
275
|
+
const visit = (node) => {
|
|
276
|
+
if (ts.isVariableDeclaration(node) && node.type && node.initializer) {
|
|
277
|
+
const type = checker.getTypeFromTypeNode(node.type);
|
|
278
|
+
checkExpressionAgainstType(node.initializer, type, checker, diagnostics);
|
|
279
|
+
}
|
|
280
|
+
else if (ts.isPropertyDeclaration(node) && node.type && node.initializer) {
|
|
281
|
+
const type = checker.getTypeFromTypeNode(node.type);
|
|
282
|
+
checkExpressionAgainstType(node.initializer, type, checker, diagnostics);
|
|
283
|
+
}
|
|
284
|
+
else if (ts.isParameter(node) && node.type && node.initializer) {
|
|
285
|
+
const type = checker.getTypeFromTypeNode(node.type);
|
|
286
|
+
checkExpressionAgainstType(node.initializer, type, checker, diagnostics);
|
|
287
|
+
}
|
|
288
|
+
ts.forEachChild(node, visit);
|
|
289
|
+
};
|
|
290
|
+
visit(sourceFile);
|
|
291
|
+
return diagnostics;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Analyze the program and patch getSemanticDiagnostics so constant constraint violations appear in tsc/IDE.
|
|
295
|
+
*/
|
|
296
|
+
export function installStaticConstraintDiagnostics(program) {
|
|
297
|
+
const checker = program.getTypeChecker();
|
|
298
|
+
const collected = [];
|
|
299
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
300
|
+
if (sourceFile.isDeclarationFile) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (sourceFile.fileName.includes('node_modules')) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
collected.push(...collectStaticConstraintDiagnostics(sourceFile, checker));
|
|
307
|
+
}
|
|
308
|
+
const previous = program.getSemanticDiagnostics.bind(program);
|
|
309
|
+
program.getSemanticDiagnostics = ((sourceFile, cancellationToken) => {
|
|
310
|
+
const base = previous(sourceFile, cancellationToken);
|
|
311
|
+
const extra = sourceFile
|
|
312
|
+
? collected.filter(d => d.file === sourceFile)
|
|
313
|
+
: collected;
|
|
314
|
+
return [...base, ...extra];
|
|
315
|
+
});
|
|
316
|
+
return collected;
|
|
317
|
+
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
export type MinLength<N extends number, Msg extends string = string> = {
|
|
2
|
-
readonly __minLength
|
|
2
|
+
readonly __minLength?: N;
|
|
3
3
|
readonly __minLength_message?: Msg;
|
|
4
4
|
};
|
|
5
5
|
export type MaxLength<N extends number, Msg extends string = string> = {
|
|
6
|
-
readonly __maxLength
|
|
6
|
+
readonly __maxLength?: N;
|
|
7
7
|
readonly __maxLength_message?: Msg;
|
|
8
8
|
};
|
|
9
9
|
export type Pattern<S extends string, Msg extends string = string> = {
|
|
10
|
-
readonly __pattern
|
|
10
|
+
readonly __pattern?: S;
|
|
11
11
|
readonly __pattern_message?: Msg;
|
|
12
12
|
};
|
|
13
13
|
export type Format<S extends 'email' | 'uuid' | 'url' | 'ipv4' | 'ipv6' | 'date' | 'date-time' | 'byte' | 'password' | 'regex' | 'hostname' | 'idn-email' | 'idn-hostname' | 'iri' | 'iri-reference' | 'uri' | 'uri-reference' | 'uri-template' | 'time' | 'duration' | 'objectId', Msg extends string = string> = {
|
|
14
|
-
readonly __format
|
|
14
|
+
readonly __format?: S;
|
|
15
15
|
readonly __format_message?: Msg;
|
|
16
16
|
};
|
|
17
17
|
export type Length<Min extends number, Max extends number> = MinLength<Min> & MaxLength<Max>;
|
|
18
18
|
export type Minimum<N extends number | bigint, Msg extends string = string> = {
|
|
19
|
-
readonly __minimum
|
|
19
|
+
readonly __minimum?: N;
|
|
20
20
|
readonly __minimum_message?: Msg;
|
|
21
21
|
};
|
|
22
22
|
export type Maximum<N extends number | bigint, Msg extends string = string> = {
|
|
23
|
-
readonly __maximum
|
|
23
|
+
readonly __maximum?: N;
|
|
24
24
|
readonly __maximum_message?: Msg;
|
|
25
25
|
};
|
|
26
26
|
export type ExclusiveMinimum<N extends number | bigint, Msg extends string = string> = {
|
|
27
|
-
readonly __exclusiveMinimum
|
|
27
|
+
readonly __exclusiveMinimum?: N;
|
|
28
28
|
readonly __exclusiveMinimum_message?: Msg;
|
|
29
29
|
};
|
|
30
30
|
export type ExclusiveMaximum<N extends number | bigint, Msg extends string = string> = {
|
|
31
|
-
readonly __exclusiveMaximum
|
|
31
|
+
readonly __exclusiveMaximum?: N;
|
|
32
32
|
readonly __exclusiveMaximum_message?: Msg;
|
|
33
33
|
};
|
|
34
34
|
export type MultipleOf<N extends number | bigint, Msg extends string = string> = {
|
|
35
|
-
readonly __multipleOf
|
|
35
|
+
readonly __multipleOf?: N;
|
|
36
36
|
readonly __multipleOf_message?: Msg;
|
|
37
37
|
};
|
|
38
38
|
export type Range<Min extends number | bigint, Max extends number | bigint> = Minimum<Min> & Maximum<Max>;
|
|
39
39
|
export type MinItems<N extends number, Msg extends string = string> = {
|
|
40
|
-
readonly __minItems
|
|
40
|
+
readonly __minItems?: N;
|
|
41
41
|
readonly __minItems_message?: Msg;
|
|
42
42
|
};
|
|
43
43
|
export type MaxItems<N extends number, Msg extends string = string> = {
|
|
44
|
-
readonly __maxItems
|
|
44
|
+
readonly __maxItems?: N;
|
|
45
45
|
readonly __maxItems_message?: Msg;
|
|
46
46
|
};
|
|
47
47
|
export type UniqueItems<Msg extends string = string> = {
|
|
48
|
-
readonly __uniqueItems
|
|
48
|
+
readonly __uniqueItems?: true;
|
|
49
49
|
readonly __uniqueItems_message?: Msg;
|
|
50
50
|
};
|
|
51
51
|
export type Custom<Fn extends (...args: any[]) => boolean, Msg extends string = string> = {
|
|
52
|
-
readonly __custom
|
|
52
|
+
readonly __custom?: Fn;
|
|
53
53
|
readonly __custom_message?: Msg;
|
|
54
54
|
};
|
|
55
55
|
export type Requires<Paths extends string | readonly string[], Msg extends string = string> = {
|
|
56
|
-
readonly __requires
|
|
56
|
+
readonly __requires?: Paths;
|
|
57
57
|
readonly __requires_message?: Msg;
|
|
58
58
|
};
|
|
59
59
|
export type Message<Msg extends string> = {
|
|
60
|
-
readonly __message
|
|
60
|
+
readonly __message?: Msg;
|
|
61
61
|
};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
export type LowerCase = {
|
|
2
|
-
readonly __transform_lowercase
|
|
2
|
+
readonly __transform_lowercase?: true;
|
|
3
3
|
};
|
|
4
4
|
export type UpperCase = {
|
|
5
|
-
readonly __transform_uppercase
|
|
5
|
+
readonly __transform_uppercase?: true;
|
|
6
6
|
};
|
|
7
7
|
export type Trim = {
|
|
8
|
-
readonly __transform_trim
|
|
8
|
+
readonly __transform_trim?: true;
|
|
9
9
|
};
|
|
10
10
|
export type Capitalize = {
|
|
11
|
-
readonly __transform_capitalize
|
|
11
|
+
readonly __transform_capitalize?: true;
|
|
12
12
|
};
|
|
13
13
|
export type ToNumber = {
|
|
14
|
-
readonly __transform_tonumber
|
|
14
|
+
readonly __transform_tonumber?: true;
|
|
15
15
|
};
|
|
16
16
|
export type ToBoolean = {
|
|
17
|
-
readonly __transform_toboolean
|
|
17
|
+
readonly __transform_toboolean?: true;
|
|
18
18
|
};
|
|
19
19
|
export type ToDate = {
|
|
20
|
-
readonly __transform_todate
|
|
20
|
+
readonly __transform_todate?: true;
|
|
21
21
|
};
|
|
22
22
|
export type Custom<Fn extends (val: any) => any> = {
|
|
23
|
-
readonly __transform_custom
|
|
23
|
+
readonly __transform_custom?: Fn;
|
|
24
24
|
};
|
package/dist/runtime/tags.d.ts
CHANGED
|
@@ -42,18 +42,15 @@ type ApplyModifiers<T, EntriesList extends any[]> = EntriesList extends [[infer
|
|
|
42
42
|
* Utility type to decorate nested properties of a type with validation/transform modifiers without retyping the structure.
|
|
43
43
|
*/
|
|
44
44
|
export type WithModifiers<T, M> = ApplyModifiers<T, TuplifyUnion<Entries<M>>>;
|
|
45
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
45
46
|
/**
|
|
46
47
|
* Recursively resolves object properties, removing the optional `?` modifier
|
|
47
48
|
* from any properties that have a `tag.Default` applied.
|
|
48
49
|
*/
|
|
49
50
|
export type ResolveDefaults<T> = T extends Date | symbol | string | number | boolean | bigint | null | undefined | Function ? T : T extends Array<infer U> ? Array<ResolveDefaults<U>> : T extends object ? {
|
|
50
|
-
[K in keyof T as
|
|
51
|
-
readonly __default: any;
|
|
52
|
-
} ? K : never]-?: ResolveDefaults<Exclude<T[K], undefined>>;
|
|
51
|
+
[K in keyof T as IsAny<T[K]> extends true ? never : '__default' extends keyof NonNullable<T[K]> ? K : never]-?: ResolveDefaults<Exclude<T[K], undefined>>;
|
|
53
52
|
} & {
|
|
54
|
-
[K in keyof T as
|
|
55
|
-
readonly __default: any;
|
|
56
|
-
} ? never : K]: ResolveDefaults<T[K]>;
|
|
53
|
+
[K in keyof T as IsAny<T[K]> extends true ? K : '__default' extends keyof NonNullable<T[K]> ? never : K]: ResolveDefaults<T[K]>;
|
|
57
54
|
} extends infer O ? {
|
|
58
55
|
[K in keyof O]: O[K];
|
|
59
56
|
} : never : T;
|
package/dist/transformer.js
CHANGED
|
@@ -2,9 +2,11 @@ import ts from './ts.js';
|
|
|
2
2
|
import { buildValidator, generateHash, buildJsonSchema, objectToAst } from './engine/resolver.js';
|
|
3
3
|
export { buildValidator, generateHash, buildJsonSchema } from './engine/resolver.js';
|
|
4
4
|
import { hoistRegistrations } from './engine/hoister.js';
|
|
5
|
+
import { installStaticConstraintDiagnostics } from './engine/staticAsserts.js';
|
|
5
6
|
const RUNTIME_FUNCTIONS = ['is', 'assert', 'assertGuard', 'validate', 'jsonSchema'];
|
|
6
7
|
export default function transformer(program) {
|
|
7
8
|
const checker = program.getTypeChecker();
|
|
9
|
+
installStaticConstraintDiagnostics(program);
|
|
8
10
|
return (context) => {
|
|
9
11
|
return (sourceFile) => {
|
|
10
12
|
const validatorCache = new Map();
|