typespec-http-zod 0.29.0 → 0.31.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/README.md +19 -0
- package/dist/src/api.d.ts +55 -0
- package/dist/src/api.js +1602 -369
- package/dist/src/constraints.d.ts +45 -1
- package/dist/src/constraints.js +242 -46
- package/dist/src/lib.d.ts +148 -7
- package/dist/src/lib.js +193 -6
- package/dist/src/published.d.ts +139 -0
- package/dist/src/published.js +798 -0
- package/dist/src/registry.d.ts +77 -45
- package/dist/src/registry.js +759 -198
- package/dist/src/types.d.ts +12 -2
- package/dist/src/types.js +276 -78
- package/dist/src/uri-template.d.ts +110 -8
- package/dist/src/uri-template.js +251 -46
- package/dist/src/zod.d.ts +732 -12
- package/dist/src/zod.js +4042 -411
- package/package.json +1 -1
|
@@ -14,7 +14,51 @@ import { type Program, type Type } from "@typespec/compiler";
|
|
|
14
14
|
* publishes. `src/tsp-index.ts` exports `$lib` and no `$decorators`, and `vocabulary.test.ts`
|
|
15
15
|
* asserts that no emitted file carries a `.refine(` outside the one multipart-file carve-out.
|
|
16
16
|
*/
|
|
17
|
-
export declare function applyConstraints(program: Program, expression: string, target: Type
|
|
17
|
+
export declare function applyConstraints(program: Program, expression: string, target: Type, options?: {
|
|
18
|
+
/**
|
|
19
|
+
* The JSON type of the value the expression is, where the caller knows it better - a member of
|
|
20
|
+
* a union, which the target's own type does not say.
|
|
21
|
+
*/
|
|
22
|
+
readonly type?: string;
|
|
23
|
+
/** Drop an inapplicable keyword without reporting it: JSON Schema skips it for this member. */
|
|
24
|
+
readonly quiet?: boolean;
|
|
25
|
+
}): string;
|
|
26
|
+
/** Whether a property or scalar declares any constraint this module enforces. */
|
|
27
|
+
export declare function hasConstraints(program: Program, target: Type): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The constraint of a property or scalar that a string value breaks - `@maxLength(3)` - or
|
|
30
|
+
* `undefined` where it keeps every one. Read as JSON Schema reads them: lengths in code points, and a
|
|
31
|
+
* pattern that matches anywhere in the value unless it anchors itself.
|
|
32
|
+
*/
|
|
33
|
+
export declare function brokenConstraintOf(program: Program, target: Type, value: string | number | boolean): string | undefined;
|
|
34
|
+
/** The `@minItems` or `@maxItems` of a property or a named list model that a list's length breaks. */
|
|
35
|
+
export declare function brokenItemsOf(program: Program, target: Type, count: number): string | undefined;
|
|
36
|
+
/** `unenforceable-encoded-constraint`, once per declaration however many positions apply it. */
|
|
37
|
+
export declare function reportUnenforceableEncoded(program: Program, target: Type, keywords: readonly string[], type: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* **Whether this diagnostic has already been raised against this declaration.**
|
|
40
|
+
*
|
|
41
|
+
* Keyed on the code as well as the target. Keying on the target alone once made the memo shared
|
|
42
|
+
* between every pattern diagnostic, so whichever ran first claimed the declaration and silenced
|
|
43
|
+
* the rest: measured on `^(\w+\s?)*\-$`, one warning where two were due. The ReDoS judgement has
|
|
44
|
+
* since moved to a linter rule, which reports per declaration by construction, so that pairing can
|
|
45
|
+
* no longer meet here - but a memo keyed on the target alone is the wrong shape for the next
|
|
46
|
+
* diagnostic added beside this one, and costs nothing to keep right.
|
|
47
|
+
*
|
|
48
|
+
* The de-duplication itself is per declaration, because emission reaches a scalar once per USE -
|
|
49
|
+
* 153 times for a single scalar in the reference service - and repeating one warning that often
|
|
50
|
+
* is noise a reader learns to skip, which is how a real one gets missed.
|
|
51
|
+
*
|
|
52
|
+
* **A declaration is its SOURCE, not the type object the walk holds.** The compiler hands out
|
|
53
|
+
* distinct objects for one declaration: `model B is A` copies `A`'s properties, a spread copies the
|
|
54
|
+
* spread model's, and each versioning projection clones the whole service - once per emitter that
|
|
55
|
+
* asks for it. Keyed on the object, one `@encode` was reported twice through `model B is A`, and
|
|
56
|
+
* every diagnostic twice when `typespec-hono` and this emitter projected one versioned service in one
|
|
57
|
+
* compile. The syntax node is shared by every copy, so it is the key where there is one. `detail`
|
|
58
|
+
* keeps two different findings at one node apart - a template property instantiated with two
|
|
59
|
+
* different types is one node and two declarations.
|
|
60
|
+
*/
|
|
61
|
+
export declare function firstReportOf(program: Program, code: string, target: Type, detail?: string): boolean;
|
|
18
62
|
/**
|
|
19
63
|
* **Whether a `@pattern` can be proven safe against catastrophic backtracking.**
|
|
20
64
|
*
|
package/dist/src/constraints.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getMaxItems, getMaxLength, getMaxValue, getMinItems, getMinLength, getMinValue, getMinValueExclusive, getMaxValueExclusive, getPattern, getMaxValueForScalar, getMinValueForScalar, serializeValueAsJson, } from "@typespec/compiler";
|
|
2
2
|
import { defaultMaxSteps, isSafePattern } from "redos-detector";
|
|
3
3
|
import { reportDiagnostic } from "./lib.js";
|
|
4
4
|
import { escapeNonAscii } from "./literal.js";
|
|
5
|
+
import { encodedValueOf, isArrayLiteral, jsonTypeOfScalar, scalarEncodingOf } from "./zod.js";
|
|
5
6
|
/**
|
|
6
7
|
* Constraint decorators -> the Zod modifiers that enforce them.
|
|
7
8
|
*
|
|
@@ -17,63 +18,243 @@ import { escapeNonAscii } from "./literal.js";
|
|
|
17
18
|
* publishes. `src/tsp-index.ts` exports `$lib` and no `$decorators`, and `vocabulary.test.ts`
|
|
18
19
|
* asserts that no emitted file carries a `.refine(` outside the one multipart-file carve-out.
|
|
19
20
|
*/
|
|
20
|
-
export function applyConstraints(program, expression, target) {
|
|
21
|
+
export function applyConstraints(program, expression, target, options = {}) {
|
|
22
|
+
// A refused value carries no method to constrain, and needs none: it accepts nothing already.
|
|
23
|
+
if (expression.startsWith("z.never()"))
|
|
24
|
+
return expression;
|
|
21
25
|
let result = expression;
|
|
22
26
|
const read = (get) => declaredOn(program, target, get);
|
|
23
27
|
const minLength = read(getMinLength);
|
|
24
28
|
const maxLength = read(getMaxLength);
|
|
25
|
-
const minValue = read(
|
|
26
|
-
const maxValue = read(
|
|
29
|
+
const minValue = read(minimumOf);
|
|
30
|
+
const maxValue = read(maximumOf);
|
|
27
31
|
const minItems = read(getMinItems);
|
|
28
32
|
const maxItems = read(getMaxItems);
|
|
29
33
|
const pattern = read(getPattern);
|
|
30
34
|
const minExclusive = read(getMinValueExclusive);
|
|
31
35
|
const maxExclusive = read(getMaxValueExclusive);
|
|
32
36
|
/**
|
|
33
|
-
* **A
|
|
34
|
-
* numeric property and makes the wire value a string, so the expression here is `z.string()` while
|
|
35
|
-
* `@minValue` is still a statement about the number it spells. Folding the two together emitted a
|
|
36
|
-
* LENGTH check: `@minValue(1)` accepted `"0"`, and `@minValue(100)` refused `"150"`. The exclusive
|
|
37
|
-
* form was worse - `z.string().gt(0)` is not a function, so the module threw on import and took
|
|
38
|
-
* the whole service down with it.
|
|
37
|
+
* **A keyword applies to one JSON type, and `@encode` can change the type under it.**
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* openapi3 applies a property's or a scalar's constraints and THEN lets the encoding replace
|
|
40
|
+
* `type`, so `@encode(string)` on a numeric property publishes `minimum` beside `type: string`,
|
|
41
|
+
* and `@encode("x", int32)` on a string publishes `pattern` beside `type: integer`. JSON Schema
|
|
42
|
+
* applies `minimum` to numbers, `minLength` and `pattern` to strings and `minItems` to arrays, and
|
|
43
|
+
* ignores each everywhere else - so the document asserts none of them there.
|
|
44
|
+
*
|
|
45
|
+
* Applied anyway, `@minValue(1)` on a string emitted a LENGTH check that accepted `"0"`, and
|
|
46
|
+
* `.gt()`, `.regex()` on an integer or `.min()` on a boolean is not a function at all: the module
|
|
47
|
+
* threw on import and took the whole service down. So each keyword the encoded value cannot carry
|
|
48
|
+
* is dropped, as the document drops it, and reported, because the author wrote a rule nothing
|
|
49
|
+
* enforces.
|
|
44
50
|
*/
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const type = options.type ?? valueJsonTypeOf(program, target);
|
|
52
|
+
const dropped = [];
|
|
53
|
+
const applies = (keyword, value, family) => {
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
if (type === undefined || family.includes(type))
|
|
57
|
+
return true;
|
|
58
|
+
dropped.push(keyword);
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
const TEXT = ["string"];
|
|
62
|
+
const NUMBER = ["integer", "number"];
|
|
63
|
+
const LIST = ["array"];
|
|
64
|
+
const keep = {
|
|
65
|
+
minLength: applies("@minLength", minLength, TEXT),
|
|
66
|
+
maxLength: applies("@maxLength", maxLength, TEXT),
|
|
67
|
+
pattern: applies("@pattern", pattern, TEXT),
|
|
68
|
+
minValue: applies("@minValue", minValue, NUMBER),
|
|
69
|
+
maxValue: applies("@maxValue", maxValue, NUMBER),
|
|
70
|
+
minExclusive: applies("@minValueExclusive", minExclusive, NUMBER),
|
|
71
|
+
maxExclusive: applies("@maxValueExclusive", maxExclusive, NUMBER),
|
|
72
|
+
minItems: applies("@minItems", minItems, LIST),
|
|
73
|
+
maxItems: applies("@maxItems", maxItems, LIST),
|
|
74
|
+
};
|
|
75
|
+
if (dropped.length > 0 && type !== undefined && options.quiet !== true) {
|
|
76
|
+
reportUnenforceableEncoded(program, target, dropped, type);
|
|
55
77
|
}
|
|
56
78
|
// `@minLength`/`@minValue`/`@minItems` all land on Zod's `.min()`; they are distinguished by the
|
|
57
|
-
// type they
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
// JSON type they apply to, and at most one of them applies to any one value.
|
|
80
|
+
const min = keep.minLength
|
|
81
|
+
? minLength
|
|
82
|
+
: keep.minValue
|
|
83
|
+
? minValue
|
|
84
|
+
: keep.minItems
|
|
85
|
+
? minItems
|
|
86
|
+
: undefined;
|
|
87
|
+
const max = keep.maxLength
|
|
88
|
+
? maxLength
|
|
89
|
+
: keep.maxValue
|
|
90
|
+
? maxValue
|
|
91
|
+
: keep.maxItems
|
|
92
|
+
? maxItems
|
|
93
|
+
: undefined;
|
|
61
94
|
if (min !== undefined)
|
|
62
95
|
result += `.min(${min})`;
|
|
63
96
|
if (max !== undefined)
|
|
64
97
|
result += `.max(${max})`;
|
|
65
98
|
// Exclusive bounds are a distinct question from inclusive ones - `z.number().positive()` is
|
|
66
99
|
// `.gt(0)`, and emitting `.min(0)` for it would accept a zero share count or a zero spot rate.
|
|
67
|
-
if (
|
|
100
|
+
if (keep.minExclusive)
|
|
68
101
|
result += `.gt(${minExclusive})`;
|
|
69
|
-
if (
|
|
102
|
+
if (keep.maxExclusive)
|
|
70
103
|
result += `.lt(${maxExclusive})`;
|
|
71
|
-
if (pattern !== undefined) {
|
|
104
|
+
if (keep.pattern && pattern !== undefined) {
|
|
72
105
|
reportNonUnicodePattern(program, pattern, target);
|
|
73
106
|
result += `.regex(${patternToRegex(pattern)})`;
|
|
74
107
|
}
|
|
75
108
|
return result;
|
|
76
109
|
}
|
|
110
|
+
/** Whether a property or scalar declares any constraint this module enforces. */
|
|
111
|
+
export function hasConstraints(program, target) {
|
|
112
|
+
if (isListLiteral(program, target))
|
|
113
|
+
return false;
|
|
114
|
+
return [
|
|
115
|
+
getMinLength,
|
|
116
|
+
getMaxLength,
|
|
117
|
+
getMinValue,
|
|
118
|
+
getMaxValue,
|
|
119
|
+
getMinValueExclusive,
|
|
120
|
+
getMaxValueExclusive,
|
|
121
|
+
getMinItems,
|
|
122
|
+
getMaxItems,
|
|
123
|
+
getPattern,
|
|
124
|
+
getMinValueForScalar,
|
|
125
|
+
getMaxValueForScalar,
|
|
126
|
+
].some((get) => get(program, target) !== undefined);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The constraint of a property or scalar that a string value breaks - `@maxLength(3)` - or
|
|
130
|
+
* `undefined` where it keeps every one. Read as JSON Schema reads them: lengths in code points, and a
|
|
131
|
+
* pattern that matches anywhere in the value unless it anchors itself.
|
|
132
|
+
*/
|
|
133
|
+
export function brokenConstraintOf(program, target, value) {
|
|
134
|
+
const read = (get) => declaredOn(program, target, get);
|
|
135
|
+
if (typeof value === "boolean")
|
|
136
|
+
return undefined;
|
|
137
|
+
if (typeof value === "number") {
|
|
138
|
+
const minimum = read(minimumOf);
|
|
139
|
+
if (minimum !== undefined && value < minimum)
|
|
140
|
+
return `'@minValue(${minimum})'`;
|
|
141
|
+
const maximum = read(maximumOf);
|
|
142
|
+
if (maximum !== undefined && value > maximum)
|
|
143
|
+
return `'@maxValue(${maximum})'`;
|
|
144
|
+
const above = read(getMinValueExclusive);
|
|
145
|
+
if (above !== undefined && value <= above)
|
|
146
|
+
return `'@minValueExclusive(${above})'`;
|
|
147
|
+
const below = read(getMaxValueExclusive);
|
|
148
|
+
if (below !== undefined && value >= below)
|
|
149
|
+
return `'@maxValueExclusive(${below})'`;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const length = [...value].length;
|
|
153
|
+
const minLength = read(getMinLength);
|
|
154
|
+
if (minLength !== undefined && length < minLength)
|
|
155
|
+
return `'@minLength(${minLength})'`;
|
|
156
|
+
const maxLength = read(getMaxLength);
|
|
157
|
+
if (maxLength !== undefined && length > maxLength)
|
|
158
|
+
return `'@maxLength(${maxLength})'`;
|
|
159
|
+
const pattern = read(getPattern);
|
|
160
|
+
if (pattern !== undefined) {
|
|
161
|
+
let expression;
|
|
162
|
+
try {
|
|
163
|
+
expression = new RegExp(pattern, "u");
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
expression = new RegExp(pattern);
|
|
167
|
+
}
|
|
168
|
+
if (!expression.test(value))
|
|
169
|
+
return `'@pattern'`;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
/** The `@minItems` or `@maxItems` of a property or a named list model that a list's length breaks. */
|
|
174
|
+
export function brokenItemsOf(program, target, count) {
|
|
175
|
+
const minItems = declaredOn(program, target, getMinItems);
|
|
176
|
+
if (minItems !== undefined && count < minItems)
|
|
177
|
+
return `'@minItems(${minItems})'`;
|
|
178
|
+
const maxItems = declaredOn(program, target, getMaxItems);
|
|
179
|
+
if (maxItems !== undefined && count > maxItems)
|
|
180
|
+
return `'@maxItems(${maxItems})'`;
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* **`@minValue` as the number the document publishes**: a numeric bound as written, and a bound on a
|
|
185
|
+
* date or duration as the value its `@encode` puts on the wire - `@minValue(utcDateTime.fromISO(...))`
|
|
186
|
+
* under `unixTimestamp` is published as `minimum: 1577836800`. This is openapi3's own
|
|
187
|
+
* `getMinValueAsJson`, which publishes nothing where the value is not a number, and so neither does
|
|
188
|
+
* this. Read through `getMinValue` alone, the date bound was dropped while the document enforced it.
|
|
189
|
+
*/
|
|
190
|
+
function minimumOf(program, target) {
|
|
191
|
+
const numeric = getMinValue(program, target);
|
|
192
|
+
if (numeric !== undefined)
|
|
193
|
+
return numeric;
|
|
194
|
+
const value = getMinValueForScalar(program, target);
|
|
195
|
+
if (value === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
const json = serializeValueAsJson(program, value, target);
|
|
198
|
+
return typeof json === "number" ? json : undefined;
|
|
199
|
+
}
|
|
200
|
+
/** `@maxValue` as the number the document publishes - see {@link minimumOf}. */
|
|
201
|
+
function maximumOf(program, target) {
|
|
202
|
+
const numeric = getMaxValue(program, target);
|
|
203
|
+
if (numeric !== undefined)
|
|
204
|
+
return numeric;
|
|
205
|
+
const value = getMaxValueForScalar(program, target);
|
|
206
|
+
if (value === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
const json = serializeValueAsJson(program, value, target);
|
|
209
|
+
return typeof json === "number" ? json : undefined;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The JSON type of the value a property or scalar's constraints are applied to, once `@encode` has
|
|
213
|
+
* had its say - or `undefined` where nothing encodes it, and TypeSpec has already checked that each
|
|
214
|
+
* keyword is legal on the type.
|
|
215
|
+
*/
|
|
216
|
+
function valueJsonTypeOf(program, target) {
|
|
217
|
+
if (target.kind === "Scalar") {
|
|
218
|
+
return scalarEncodingOf(program, target) === undefined
|
|
219
|
+
? undefined
|
|
220
|
+
: jsonTypeOfScalar(program, target);
|
|
221
|
+
}
|
|
222
|
+
if (target.kind !== "ModelProperty")
|
|
223
|
+
return undefined;
|
|
224
|
+
const encoding = encodedValueOf(program, target);
|
|
225
|
+
if (encoding.kind === "replaced" || encoding.kind === "both") {
|
|
226
|
+
return jsonTypeOfScalar(program, encoding.scalar);
|
|
227
|
+
}
|
|
228
|
+
if (encoding.kind === "unsatisfiable")
|
|
229
|
+
return undefined;
|
|
230
|
+
const declared = nullableInnerOfType(target.type) ?? target.type;
|
|
231
|
+
return declared.kind === "Scalar" && scalarEncodingOf(program, declared) !== undefined
|
|
232
|
+
? jsonTypeOfScalar(program, declared)
|
|
233
|
+
: undefined;
|
|
234
|
+
}
|
|
235
|
+
/** The non-null half of a two-variant `T | null` union. */
|
|
236
|
+
function nullableInnerOfType(type) {
|
|
237
|
+
if (type.kind !== "Union")
|
|
238
|
+
return undefined;
|
|
239
|
+
const variants = [...type.variants.values()].map((variant) => variant.type);
|
|
240
|
+
const real = variants.filter((v) => !(v.kind === "Intrinsic" && v.name === "null"));
|
|
241
|
+
return real.length === variants.length - 1 && real.length === 1 ? real[0] : undefined;
|
|
242
|
+
}
|
|
243
|
+
/** `unenforceable-encoded-constraint`, once per declaration however many positions apply it. */
|
|
244
|
+
export function reportUnenforceableEncoded(program, target, keywords, type) {
|
|
245
|
+
const detail = `${keywords.join(",")} ${type}`;
|
|
246
|
+
if (!firstReportOf(program, "unenforceable-encoded-constraint", target, detail))
|
|
247
|
+
return;
|
|
248
|
+
reportDiagnostic(program, {
|
|
249
|
+
code: "unenforceable-encoded-constraint",
|
|
250
|
+
target,
|
|
251
|
+
format: {
|
|
252
|
+
name: target.kind === "ModelProperty" || target.kind === "Scalar" ? target.name : "",
|
|
253
|
+
keywords: keywords.map((keyword) => `'${keyword}'`).join(", "),
|
|
254
|
+
type,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
77
258
|
/**
|
|
78
259
|
* **A constraint on a scalar is read up its `extends` chain: the nearest scalar declaring it wins.**
|
|
79
260
|
*
|
|
@@ -89,6 +270,8 @@ export function applyConstraints(program, expression, target) {
|
|
|
89
270
|
* A property is not a chain: its type's constraints are applied where the type is rendered.
|
|
90
271
|
*/
|
|
91
272
|
function declaredOn(program, target, get) {
|
|
273
|
+
if (isListLiteral(program, target))
|
|
274
|
+
return undefined;
|
|
92
275
|
if (target.kind !== "Scalar")
|
|
93
276
|
return get(program, target);
|
|
94
277
|
for (let current = target; current !== undefined; current = current.baseScalar) {
|
|
@@ -99,18 +282,16 @@ function declaredOn(program, target, get) {
|
|
|
99
282
|
return undefined;
|
|
100
283
|
}
|
|
101
284
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
285
|
+
* **A list openapi3 takes for a literal publishes none of its own keywords**, so none is enforced. A
|
|
286
|
+
* list model the spec names `Array` is one - see `isArrayLiteral` - and openapi3 writes it out where
|
|
287
|
+
* it is used as `{ type: "array", items }` and nothing else, its `@minItems`, `@maxItems`, `@doc` and
|
|
288
|
+
* `@summary` dropped. Measured on `@minItems(1) model Array is int32[]` as `a?: Array`: the document
|
|
289
|
+
* accepts `{ a: [] }`, and the validator, `z.array(z.int32()).min(1)`, refused it - in a body, a
|
|
290
|
+
* response, a query and a header alike. A keyword on the property holding it is the property's own,
|
|
291
|
+
* and is published.
|
|
105
292
|
*/
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
return false;
|
|
109
|
-
const own = getEncode(program, target);
|
|
110
|
-
const scalar = own === undefined && target.kind === "ModelProperty" && target.type.kind === "Scalar"
|
|
111
|
-
? getEncode(program, target.type)
|
|
112
|
-
: undefined;
|
|
113
|
-
return (own ?? scalar)?.type.name === "string";
|
|
293
|
+
function isListLiteral(program, target) {
|
|
294
|
+
return target.kind === "Model" && isArrayLiteral(program, target);
|
|
114
295
|
}
|
|
115
296
|
/**
|
|
116
297
|
* How much analysis one pattern gets before the answer is "could not prove it": a number of steps.
|
|
@@ -159,8 +340,17 @@ const reported = new WeakMap();
|
|
|
159
340
|
* The de-duplication itself is per declaration, because emission reaches a scalar once per USE -
|
|
160
341
|
* 153 times for a single scalar in the reference service - and repeating one warning that often
|
|
161
342
|
* is noise a reader learns to skip, which is how a real one gets missed.
|
|
343
|
+
*
|
|
344
|
+
* **A declaration is its SOURCE, not the type object the walk holds.** The compiler hands out
|
|
345
|
+
* distinct objects for one declaration: `model B is A` copies `A`'s properties, a spread copies the
|
|
346
|
+
* spread model's, and each versioning projection clones the whole service - once per emitter that
|
|
347
|
+
* asks for it. Keyed on the object, one `@encode` was reported twice through `model B is A`, and
|
|
348
|
+
* every diagnostic twice when `typespec-hono` and this emitter projected one versioned service in one
|
|
349
|
+
* compile. The syntax node is shared by every copy, so it is the key where there is one. `detail`
|
|
350
|
+
* keeps two different findings at one node apart - a template property instantiated with two
|
|
351
|
+
* different types is one node and two declarations.
|
|
162
352
|
*/
|
|
163
|
-
function firstReportOf(program, code, target) {
|
|
353
|
+
export function firstReportOf(program, code, target, detail = "") {
|
|
164
354
|
let byCode = reported.get(program);
|
|
165
355
|
if (byCode === undefined) {
|
|
166
356
|
byCode = new Map();
|
|
@@ -168,12 +358,18 @@ function firstReportOf(program, code, target) {
|
|
|
168
358
|
}
|
|
169
359
|
let already = byCode.get(code);
|
|
170
360
|
if (already === undefined) {
|
|
171
|
-
already = new
|
|
361
|
+
already = new WeakMap();
|
|
172
362
|
byCode.set(code, already);
|
|
173
363
|
}
|
|
174
|
-
|
|
364
|
+
const source = target.node ?? target;
|
|
365
|
+
let details = already.get(source);
|
|
366
|
+
if (details === undefined) {
|
|
367
|
+
details = new Set();
|
|
368
|
+
already.set(source, details);
|
|
369
|
+
}
|
|
370
|
+
if (details.has(detail))
|
|
175
371
|
return false;
|
|
176
|
-
|
|
372
|
+
details.add(detail);
|
|
177
373
|
return true;
|
|
178
374
|
}
|
|
179
375
|
/**
|