@tsonic/frontend 0.0.25 → 0.0.26
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/.tsbuildinfo +1 -1
- package/dist/ir/types/statements.d.ts +12 -0
- package/dist/ir/types/statements.d.ts.map +1 -1
- package/dist/ir/validation/attribute-collection-pass.d.ts +14 -9
- package/dist/ir/validation/attribute-collection-pass.d.ts.map +1 -1
- package/dist/ir/validation/attribute-collection-pass.js +656 -163
- package/dist/ir/validation/attribute-collection-pass.js.map +1 -1
- package/dist/ir/validation/attribute-collection-pass.test.js +325 -10
- package/dist/ir/validation/attribute-collection-pass.test.js.map +1 -1
- package/dist/types/diagnostic.d.ts +1 -1
- package/dist/types/diagnostic.d.ts.map +1 -1
- package/dist/types/diagnostic.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Attribute Collection Pass
|
|
3
3
|
*
|
|
4
|
-
* This pass detects marker calls
|
|
5
|
-
*
|
|
4
|
+
* This pass detects compiler-only marker calls and transforms them into IR attributes
|
|
5
|
+
* attached to the corresponding declarations, removing the marker statements.
|
|
6
6
|
*
|
|
7
7
|
* Supported patterns:
|
|
8
|
-
* - A.on(Class).type.add(
|
|
9
|
-
* - A.on(Class).
|
|
10
|
-
* - A.on(
|
|
8
|
+
* - A.on(Class).type.add(AttrCtor, ...args) - Type attribute
|
|
9
|
+
* - A.on(Class).ctor.add(AttrCtor, ...args) - Constructor attribute
|
|
10
|
+
* - A.on(Class).method(x => x.method).add(AttrCtor) - Method attribute
|
|
11
|
+
* - A.on(Class).prop(x => x.prop).add(AttrCtor) - Property attribute
|
|
12
|
+
* - add(A.attr(AttrCtor, ...args)) - Descriptor form
|
|
13
|
+
* - add(descriptor) where `const descriptor = A.attr(...)`
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
* - A.on(
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* Backward compatibility:
|
|
16
|
+
* - A.on(fn).type.add(AttrCtor, ...args) attaches to a function declaration
|
|
17
|
+
*
|
|
18
|
+
* Notes:
|
|
19
|
+
* - This API is compiler-only. All recognized marker statements are removed.
|
|
20
|
+
* - Invalid marker calls are errors (no silent drops).
|
|
16
21
|
*/
|
|
17
22
|
import { createDiagnostic, } from "../../types/diagnostic.js";
|
|
23
|
+
const ATTRIBUTES_IMPORT_SPECIFIER = "@tsonic/core/attributes.js";
|
|
18
24
|
/**
|
|
19
25
|
* Try to extract an attribute argument from an IR expression.
|
|
20
26
|
* Returns undefined if the expression is not a valid attribute argument.
|
|
@@ -51,130 +57,476 @@ const tryExtractAttributeArg = (expr) => {
|
|
|
51
57
|
}
|
|
52
58
|
return undefined;
|
|
53
59
|
};
|
|
60
|
+
const getAttributesApiLocalNames = (module) => {
|
|
61
|
+
const names = new Set();
|
|
62
|
+
for (const imp of module.imports) {
|
|
63
|
+
if (imp.source !== ATTRIBUTES_IMPORT_SPECIFIER)
|
|
64
|
+
continue;
|
|
65
|
+
for (const spec of imp.specifiers) {
|
|
66
|
+
if (spec.kind !== "named")
|
|
67
|
+
continue;
|
|
68
|
+
if (spec.name !== "attributes")
|
|
69
|
+
continue;
|
|
70
|
+
names.add(spec.localName);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return names;
|
|
74
|
+
};
|
|
75
|
+
const isAttributesApiIdentifier = (expr, apiNames) => expr.kind === "identifier" && apiNames.has(expr.name);
|
|
76
|
+
const getMemberName = (expr) => {
|
|
77
|
+
if (expr.kind !== "memberAccess")
|
|
78
|
+
return undefined;
|
|
79
|
+
if (expr.isComputed)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (typeof expr.property !== "string")
|
|
82
|
+
return undefined;
|
|
83
|
+
return expr.property;
|
|
84
|
+
};
|
|
85
|
+
const looksLikeAttributesApiUsage = (expr, apiNames) => {
|
|
86
|
+
switch (expr.kind) {
|
|
87
|
+
case "call":
|
|
88
|
+
return (looksLikeAttributesApiUsage(expr.callee, apiNames) ||
|
|
89
|
+
expr.arguments.some((arg) => arg.kind !== "spread" && looksLikeAttributesApiUsage(arg, apiNames)));
|
|
90
|
+
case "memberAccess":
|
|
91
|
+
return (looksLikeAttributesApiUsage(expr.object, apiNames) ||
|
|
92
|
+
(typeof expr.property === "string" &&
|
|
93
|
+
(expr.property === "on" || expr.property === "attr") &&
|
|
94
|
+
isAttributesApiIdentifier(expr.object, apiNames)));
|
|
95
|
+
case "arrowFunction":
|
|
96
|
+
return ((expr.body.kind === "blockStatement"
|
|
97
|
+
? expr.body.statements.some((s) => s.kind === "expressionStatement" &&
|
|
98
|
+
looksLikeAttributesApiUsage(s.expression, apiNames))
|
|
99
|
+
: looksLikeAttributesApiUsage(expr.body, apiNames)) || false);
|
|
100
|
+
case "functionExpression":
|
|
101
|
+
return expr.body.statements.some((s) => s.kind === "expressionStatement" &&
|
|
102
|
+
looksLikeAttributesApiUsage(s.expression, apiNames));
|
|
103
|
+
case "array":
|
|
104
|
+
return expr.elements.some((el) => el !== undefined && el.kind !== "spread" && looksLikeAttributesApiUsage(el, apiNames));
|
|
105
|
+
case "object":
|
|
106
|
+
return expr.properties.some((p) => {
|
|
107
|
+
if (p.kind === "spread")
|
|
108
|
+
return looksLikeAttributesApiUsage(p.expression, apiNames);
|
|
109
|
+
if (typeof p.key !== "string")
|
|
110
|
+
return looksLikeAttributesApiUsage(p.key, apiNames);
|
|
111
|
+
return looksLikeAttributesApiUsage(p.value, apiNames);
|
|
112
|
+
});
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const parseOnCall = (expr, module, apiNames) => {
|
|
118
|
+
if (expr.kind !== "call")
|
|
119
|
+
return { kind: "notMatch" };
|
|
120
|
+
const call = expr;
|
|
121
|
+
if (call.callee.kind !== "memberAccess")
|
|
122
|
+
return { kind: "notMatch" };
|
|
123
|
+
const member = call.callee;
|
|
124
|
+
if (member.isComputed || typeof member.property !== "string")
|
|
125
|
+
return { kind: "notMatch" };
|
|
126
|
+
if (member.property !== "on")
|
|
127
|
+
return { kind: "notMatch" };
|
|
128
|
+
if (!isAttributesApiIdentifier(member.object, apiNames))
|
|
129
|
+
return { kind: "notMatch" };
|
|
130
|
+
if (call.arguments.length !== 1) {
|
|
131
|
+
return {
|
|
132
|
+
kind: "error",
|
|
133
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: A.on(...) expects exactly 1 argument`, createLocation(module.filePath, call.sourceSpan)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const arg0 = call.arguments[0];
|
|
137
|
+
if (!arg0 || arg0.kind === "spread") {
|
|
138
|
+
return {
|
|
139
|
+
kind: "error",
|
|
140
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: A.on(...) does not accept spread arguments`, createLocation(module.filePath, call.sourceSpan)),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (arg0.kind !== "identifier") {
|
|
144
|
+
return {
|
|
145
|
+
kind: "error",
|
|
146
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: A.on(Target) target must be an identifier`, createLocation(module.filePath, call.sourceSpan)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
kind: "ok",
|
|
151
|
+
value: { target: arg0, sourceSpan: call.sourceSpan },
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const parseSelector = (selector, module) => {
|
|
155
|
+
if (selector.kind !== "arrowFunction") {
|
|
156
|
+
return {
|
|
157
|
+
kind: "error",
|
|
158
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector must be an arrow function (x => x.member)`, createLocation(module.filePath, selector.sourceSpan)),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const fn = selector;
|
|
162
|
+
if (fn.parameters.length !== 1) {
|
|
163
|
+
return {
|
|
164
|
+
kind: "error",
|
|
165
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector must have exactly 1 parameter`, createLocation(module.filePath, fn.sourceSpan)),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const p0 = fn.parameters[0];
|
|
169
|
+
if (!p0 || p0.pattern.kind !== "identifierPattern") {
|
|
170
|
+
return {
|
|
171
|
+
kind: "error",
|
|
172
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector parameter must be an identifier`, createLocation(module.filePath, fn.sourceSpan)),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const paramName = p0.pattern.name;
|
|
176
|
+
if (fn.body.kind !== "memberAccess") {
|
|
177
|
+
return {
|
|
178
|
+
kind: "error",
|
|
179
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector body must be a member access (x => x.member)`, createLocation(module.filePath, fn.sourceSpan)),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const body = fn.body;
|
|
183
|
+
if (body.isComputed || typeof body.property !== "string") {
|
|
184
|
+
return {
|
|
185
|
+
kind: "error",
|
|
186
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector must access a named member (no computed access)`, createLocation(module.filePath, fn.sourceSpan)),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (body.object.kind !== "identifier" || body.object.name !== paramName) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "error",
|
|
192
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector must be of the form (x) => x.member`, createLocation(module.filePath, fn.sourceSpan)),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { kind: "ok", value: body.property };
|
|
196
|
+
};
|
|
197
|
+
const resolveClrTypeForAttributeCtor = (ctorIdent, module) => {
|
|
198
|
+
if (ctorIdent.resolvedClrType)
|
|
199
|
+
return ctorIdent.resolvedClrType;
|
|
200
|
+
// Prefer CLR imports: these are the authoritative mapping for runtime type names.
|
|
201
|
+
for (const imp of module.imports) {
|
|
202
|
+
if (!imp.isClr)
|
|
203
|
+
continue;
|
|
204
|
+
if (!imp.resolvedNamespace)
|
|
205
|
+
continue;
|
|
206
|
+
for (const spec of imp.specifiers) {
|
|
207
|
+
if (spec.kind !== "named")
|
|
208
|
+
continue;
|
|
209
|
+
if (spec.localName !== ctorIdent.name)
|
|
210
|
+
continue;
|
|
211
|
+
return `${imp.resolvedNamespace}.${spec.name}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
};
|
|
216
|
+
const makeAttributeType = (ctorIdent, module) => {
|
|
217
|
+
const resolvedClrType = resolveClrTypeForAttributeCtor(ctorIdent, module);
|
|
218
|
+
if (resolvedClrType) {
|
|
219
|
+
return {
|
|
220
|
+
kind: "ok",
|
|
221
|
+
value: {
|
|
222
|
+
kind: "referenceType",
|
|
223
|
+
name: ctorIdent.name,
|
|
224
|
+
resolvedClrType,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Allow locally-emitted attribute types (non-ambient class declarations).
|
|
229
|
+
const hasLocalClass = module.body.some((s) => s.kind === "classDeclaration" && s.name === ctorIdent.name);
|
|
230
|
+
if (hasLocalClass) {
|
|
231
|
+
return { kind: "ok", value: { kind: "referenceType", name: ctorIdent.name } };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
kind: "error",
|
|
235
|
+
diagnostic: createDiagnostic("TSN4004", "error", `Missing CLR binding for attribute constructor '${ctorIdent.name}'. Import the attribute type from a CLR bindings module (e.g., @tsonic/dotnet) or define it as a local class.`, createLocation(module.filePath, ctorIdent.sourceSpan)),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const parseAttrDescriptorCall = (expr, module, apiNames) => {
|
|
239
|
+
if (expr.kind !== "call")
|
|
240
|
+
return { kind: "notMatch" };
|
|
241
|
+
const call = expr;
|
|
242
|
+
if (call.callee.kind !== "memberAccess")
|
|
243
|
+
return { kind: "notMatch" };
|
|
244
|
+
const member = call.callee;
|
|
245
|
+
if (member.isComputed || typeof member.property !== "string")
|
|
246
|
+
return { kind: "notMatch" };
|
|
247
|
+
if (member.property !== "attr")
|
|
248
|
+
return { kind: "notMatch" };
|
|
249
|
+
if (!isAttributesApiIdentifier(member.object, apiNames))
|
|
250
|
+
return { kind: "notMatch" };
|
|
251
|
+
if (call.arguments.length < 1) {
|
|
252
|
+
return {
|
|
253
|
+
kind: "error",
|
|
254
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: A.attr(AttrCtor, ...args) requires at least the attribute constructor`, createLocation(module.filePath, call.sourceSpan)),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const rawArgs = [];
|
|
258
|
+
for (const arg of call.arguments) {
|
|
259
|
+
if (!arg || arg.kind === "spread") {
|
|
260
|
+
return {
|
|
261
|
+
kind: "error",
|
|
262
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: spreads are not allowed in attributes`, createLocation(module.filePath, call.sourceSpan)),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
rawArgs.push(arg);
|
|
266
|
+
}
|
|
267
|
+
const ctorExpr = rawArgs[0];
|
|
268
|
+
if (!ctorExpr || ctorExpr.kind !== "identifier") {
|
|
269
|
+
return {
|
|
270
|
+
kind: "error",
|
|
271
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: A.attr(...) attribute constructor must be an identifier`, createLocation(module.filePath, call.sourceSpan)),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const attributeCtor = ctorExpr;
|
|
275
|
+
const attributeTypeResult = makeAttributeType(attributeCtor, module);
|
|
276
|
+
if (attributeTypeResult.kind !== "ok") {
|
|
277
|
+
return attributeTypeResult;
|
|
278
|
+
}
|
|
279
|
+
const positionalArgs = [];
|
|
280
|
+
const namedArgs = new Map();
|
|
281
|
+
let sawNamed = false;
|
|
282
|
+
for (const arg of rawArgs.slice(1)) {
|
|
283
|
+
if (arg.kind === "object") {
|
|
284
|
+
sawNamed = true;
|
|
285
|
+
const obj = arg;
|
|
286
|
+
for (const prop of obj.properties) {
|
|
287
|
+
if (prop.kind === "spread") {
|
|
288
|
+
return {
|
|
289
|
+
kind: "error",
|
|
290
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: spreads are not allowed in named arguments`, createLocation(module.filePath, obj.sourceSpan)),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (prop.kind !== "property" || typeof prop.key !== "string") {
|
|
294
|
+
return {
|
|
295
|
+
kind: "error",
|
|
296
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: named arguments must be simple { Name: value } properties`, createLocation(module.filePath, obj.sourceSpan)),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const v = tryExtractAttributeArg(prop.value);
|
|
300
|
+
if (!v) {
|
|
301
|
+
return {
|
|
302
|
+
kind: "error",
|
|
303
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: named argument '${prop.key}' must be a compile-time constant`, createLocation(module.filePath, prop.value.sourceSpan)),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
namedArgs.set(prop.key, v);
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (sawNamed) {
|
|
311
|
+
return {
|
|
312
|
+
kind: "error",
|
|
313
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: positional arguments cannot appear after named arguments`, createLocation(module.filePath, arg.sourceSpan)),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const v = tryExtractAttributeArg(arg);
|
|
317
|
+
if (!v) {
|
|
318
|
+
return {
|
|
319
|
+
kind: "error",
|
|
320
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: attribute arguments must be compile-time constants (string/number/boolean/typeof/enum)`, createLocation(module.filePath, arg.sourceSpan)),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
positionalArgs.push(v);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
kind: "ok",
|
|
327
|
+
value: {
|
|
328
|
+
attributeType: attributeTypeResult.value,
|
|
329
|
+
positionalArgs,
|
|
330
|
+
namedArgs,
|
|
331
|
+
sourceSpan: call.sourceSpan,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
};
|
|
54
335
|
/**
|
|
55
336
|
* Try to detect if a call expression is an attribute marker pattern.
|
|
56
337
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* -
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* - object: CallExpression - the A.on(Target) call
|
|
63
|
-
* - callee: MemberAccess with property "on"
|
|
64
|
-
* - object: Identifier "A" or "attributes"
|
|
65
|
-
* - arguments: [Target identifier]
|
|
66
|
-
* - arguments: [Attr type, ...positional args]
|
|
338
|
+
* Patterns:
|
|
339
|
+
* - A.on(Target).type.add(...)
|
|
340
|
+
* - A.on(Target).ctor.add(...)
|
|
341
|
+
* - A.on(Target).method(selector).add(...)
|
|
342
|
+
* - A.on(Target).prop(selector).add(...)
|
|
67
343
|
*/
|
|
68
|
-
const tryDetectAttributeMarker = (call, module) => {
|
|
69
|
-
// Check outer call: must be a member access call like .add(...)
|
|
344
|
+
const tryDetectAttributeMarker = (call, module, apiNames, descriptors) => {
|
|
70
345
|
if (call.callee.kind !== "memberAccess")
|
|
71
|
-
return
|
|
346
|
+
return { kind: "notMatch" };
|
|
72
347
|
const outerMember = call.callee;
|
|
73
|
-
// Check that property is "add" (string, not computed)
|
|
74
348
|
if (outerMember.isComputed || typeof outerMember.property !== "string") {
|
|
75
|
-
return
|
|
349
|
+
return { kind: "notMatch" };
|
|
76
350
|
}
|
|
77
351
|
if (outerMember.property !== "add")
|
|
78
|
-
return
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
352
|
+
return { kind: "notMatch" };
|
|
353
|
+
// Determine the target selector: `.type`, `.ctor`, `.method(selector)`, `.prop(selector)`
|
|
354
|
+
let selector;
|
|
355
|
+
let selectedMemberName;
|
|
356
|
+
let onCallExpr;
|
|
357
|
+
if (outerMember.object.kind === "memberAccess") {
|
|
358
|
+
const targetMember = outerMember.object;
|
|
359
|
+
const prop = getMemberName(targetMember);
|
|
360
|
+
if (prop !== "type" && prop !== "ctor")
|
|
361
|
+
return { kind: "notMatch" };
|
|
362
|
+
selector = prop;
|
|
363
|
+
onCallExpr = targetMember.object;
|
|
85
364
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
365
|
+
else if (outerMember.object.kind === "call") {
|
|
366
|
+
const selectorCall = outerMember.object;
|
|
367
|
+
if (selectorCall.callee.kind !== "memberAccess")
|
|
368
|
+
return { kind: "notMatch" };
|
|
369
|
+
const selectorMember = selectorCall.callee;
|
|
370
|
+
const prop = getMemberName(selectorMember);
|
|
371
|
+
if (prop !== "method" && prop !== "prop")
|
|
372
|
+
return { kind: "notMatch" };
|
|
373
|
+
selector = prop;
|
|
374
|
+
if (selectorCall.arguments.length !== 1) {
|
|
375
|
+
return {
|
|
376
|
+
kind: "error",
|
|
377
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: .${prop}(selector) expects exactly 1 argument`, createLocation(module.filePath, selectorCall.sourceSpan)),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const arg0 = selectorCall.arguments[0];
|
|
381
|
+
if (!arg0 || arg0.kind === "spread") {
|
|
382
|
+
return {
|
|
383
|
+
kind: "error",
|
|
384
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: selector cannot be a spread argument`, createLocation(module.filePath, selectorCall.sourceSpan)),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const sel = parseSelector(arg0, module);
|
|
388
|
+
if (sel.kind !== "ok")
|
|
389
|
+
return sel;
|
|
390
|
+
selectedMemberName = sel.value;
|
|
391
|
+
onCallExpr = selectorMember.object;
|
|
98
392
|
}
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
// Check that the object of .on is "A" or "attributes"
|
|
102
|
-
if (onMember.object.kind !== "identifier")
|
|
103
|
-
return undefined;
|
|
104
|
-
const apiObject = onMember.object;
|
|
105
|
-
if (apiObject.name !== "A" && apiObject.name !== "attributes")
|
|
106
|
-
return undefined;
|
|
107
|
-
// Extract target from A.on(Target)
|
|
108
|
-
if (onCall.arguments.length !== 1)
|
|
109
|
-
return undefined;
|
|
110
|
-
const targetArg = onCall.arguments[0];
|
|
111
|
-
if (!targetArg || targetArg.kind === "spread")
|
|
112
|
-
return undefined;
|
|
113
|
-
// Target must be an identifier
|
|
114
|
-
if (targetArg.kind !== "identifier")
|
|
115
|
-
return undefined;
|
|
116
|
-
const targetName = targetArg.name;
|
|
117
|
-
// Extract attribute type from .add(Attr, ...) arguments
|
|
118
|
-
if (call.arguments.length < 1)
|
|
119
|
-
return undefined;
|
|
120
|
-
const attrTypeArg = call.arguments[0];
|
|
121
|
-
if (!attrTypeArg || attrTypeArg.kind === "spread")
|
|
122
|
-
return undefined;
|
|
123
|
-
// Attribute type should be an identifier referencing the attribute class
|
|
124
|
-
if (attrTypeArg.kind !== "identifier") {
|
|
125
|
-
// Not a simple identifier - could be a member access like System.SerializableAttribute
|
|
126
|
-
// For now, we don't support this
|
|
127
|
-
return undefined;
|
|
393
|
+
else {
|
|
394
|
+
return { kind: "notMatch" };
|
|
128
395
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
396
|
+
if (!onCallExpr)
|
|
397
|
+
return { kind: "notMatch" };
|
|
398
|
+
const on = parseOnCall(onCallExpr, module, apiNames);
|
|
399
|
+
if (on.kind !== "ok")
|
|
400
|
+
return on;
|
|
401
|
+
const targetName = on.value.target.name;
|
|
402
|
+
// Parse `.add(...)` arguments
|
|
403
|
+
if (call.arguments.length < 1) {
|
|
404
|
+
return {
|
|
405
|
+
kind: "error",
|
|
406
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: .add(...) requires at least one argument`, createLocation(module.filePath, call.sourceSpan)),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const addArgs = [];
|
|
410
|
+
for (const arg of call.arguments) {
|
|
411
|
+
if (!arg || arg.kind === "spread") {
|
|
412
|
+
return {
|
|
413
|
+
kind: "error",
|
|
414
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: spreads are not allowed in attributes`, createLocation(module.filePath, call.sourceSpan)),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
addArgs.push(arg);
|
|
418
|
+
}
|
|
419
|
+
const first = addArgs[0];
|
|
420
|
+
if (!first) {
|
|
421
|
+
return {
|
|
422
|
+
kind: "error",
|
|
423
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: .add(...) first argument is missing`, createLocation(module.filePath, call.sourceSpan)),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// .add(A.attr(...)) inline descriptor
|
|
427
|
+
if (addArgs.length === 1) {
|
|
428
|
+
const descCall = parseAttrDescriptorCall(first, module, apiNames);
|
|
429
|
+
if (descCall.kind === "ok") {
|
|
430
|
+
return {
|
|
431
|
+
kind: "ok",
|
|
432
|
+
value: {
|
|
433
|
+
targetName,
|
|
434
|
+
targetSelector: selector,
|
|
435
|
+
selectedMemberName,
|
|
436
|
+
attributeType: descCall.value.attributeType,
|
|
437
|
+
positionalArgs: descCall.value.positionalArgs,
|
|
438
|
+
namedArgs: descCall.value.namedArgs,
|
|
439
|
+
sourceSpan: call.sourceSpan,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// .add(descriptorVar)
|
|
444
|
+
if (first.kind === "identifier") {
|
|
445
|
+
const desc = descriptors.get(first.name);
|
|
446
|
+
if (desc) {
|
|
447
|
+
return {
|
|
448
|
+
kind: "ok",
|
|
449
|
+
value: {
|
|
450
|
+
targetName,
|
|
451
|
+
targetSelector: selector,
|
|
452
|
+
selectedMemberName,
|
|
453
|
+
attributeType: desc.attributeType,
|
|
454
|
+
positionalArgs: desc.positionalArgs,
|
|
455
|
+
namedArgs: desc.namedArgs,
|
|
456
|
+
sourceSpan: call.sourceSpan,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
144
459
|
}
|
|
145
460
|
}
|
|
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
|
-
|
|
461
|
+
}
|
|
462
|
+
// .add(AttrCtor, ...args)
|
|
463
|
+
if (first.kind !== "identifier") {
|
|
464
|
+
return {
|
|
465
|
+
kind: "error",
|
|
466
|
+
diagnostic: createDiagnostic("TSN4005", "error", `Invalid attribute marker: .add(AttrCtor, ...args) requires attribute constructor to be an identifier`, createLocation(module.filePath, call.sourceSpan)),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const attributeTypeResult = makeAttributeType(first, module);
|
|
470
|
+
if (attributeTypeResult.kind !== "ok") {
|
|
471
|
+
return attributeTypeResult;
|
|
472
|
+
}
|
|
473
|
+
const positionalArgs = [];
|
|
474
|
+
const namedArgs = new Map();
|
|
475
|
+
let sawNamed = false;
|
|
476
|
+
for (const arg of addArgs.slice(1)) {
|
|
477
|
+
if (arg.kind === "object") {
|
|
478
|
+
sawNamed = true;
|
|
479
|
+
const obj = arg;
|
|
480
|
+
for (const prop of obj.properties) {
|
|
481
|
+
if (prop.kind === "spread") {
|
|
482
|
+
return {
|
|
483
|
+
kind: "error",
|
|
484
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: spreads are not allowed in named arguments`, createLocation(module.filePath, obj.sourceSpan)),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (prop.kind !== "property" || typeof prop.key !== "string") {
|
|
488
|
+
return {
|
|
489
|
+
kind: "error",
|
|
490
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: named arguments must be simple { Name: value } properties`, createLocation(module.filePath, obj.sourceSpan)),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const v = tryExtractAttributeArg(prop.value);
|
|
494
|
+
if (!v) {
|
|
495
|
+
return {
|
|
496
|
+
kind: "error",
|
|
497
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: named argument '${prop.key}' must be a compile-time constant`, createLocation(module.filePath, prop.value.sourceSpan)),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
namedArgs.set(prop.key, v);
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (sawNamed) {
|
|
505
|
+
return {
|
|
506
|
+
kind: "error",
|
|
507
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: positional arguments cannot appear after named arguments`, createLocation(module.filePath, arg.sourceSpan)),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const v = tryExtractAttributeArg(arg);
|
|
511
|
+
if (!v) {
|
|
512
|
+
return {
|
|
513
|
+
kind: "error",
|
|
514
|
+
diagnostic: createDiagnostic("TSN4006", "error", `Invalid attribute argument: attribute arguments must be compile-time constants (string/number/boolean/typeof/enum)`, createLocation(module.filePath, arg.sourceSpan)),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
positionalArgs.push(v);
|
|
518
|
+
}
|
|
171
519
|
return {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
520
|
+
kind: "ok",
|
|
521
|
+
value: {
|
|
522
|
+
targetName,
|
|
523
|
+
targetSelector: selector,
|
|
524
|
+
selectedMemberName,
|
|
525
|
+
attributeType: attributeTypeResult.value,
|
|
526
|
+
positionalArgs,
|
|
527
|
+
namedArgs,
|
|
528
|
+
sourceSpan: call.sourceSpan,
|
|
529
|
+
},
|
|
178
530
|
};
|
|
179
531
|
};
|
|
180
532
|
/**
|
|
@@ -185,24 +537,72 @@ const createLocation = (filePath, sourceSpan) => sourceSpan ?? { file: filePath,
|
|
|
185
537
|
* Process a single module: detect attribute markers and attach to declarations
|
|
186
538
|
*/
|
|
187
539
|
const processModule = (module, diagnostics) => {
|
|
540
|
+
const apiNames = getAttributesApiLocalNames(module);
|
|
541
|
+
if (apiNames.size === 0) {
|
|
542
|
+
return module;
|
|
543
|
+
}
|
|
544
|
+
// Collect detected attribute descriptors declared as variables:
|
|
545
|
+
// const d = A.attr(AttrCtor, ...args)
|
|
546
|
+
const descriptors = new Map();
|
|
547
|
+
const removedStatementIndices = new Set();
|
|
548
|
+
module.body.forEach((stmt, i) => {
|
|
549
|
+
if (stmt.kind !== "variableDeclaration")
|
|
550
|
+
return;
|
|
551
|
+
const decl = stmt;
|
|
552
|
+
// Only handle simple, single declarator `const name = A.attr(...)`.
|
|
553
|
+
if (decl.declarationKind !== "const")
|
|
554
|
+
return;
|
|
555
|
+
if (decl.declarations.length !== 1)
|
|
556
|
+
return;
|
|
557
|
+
const d0 = decl.declarations[0];
|
|
558
|
+
if (!d0)
|
|
559
|
+
return;
|
|
560
|
+
if (d0.name.kind !== "identifierPattern")
|
|
561
|
+
return;
|
|
562
|
+
if (!d0.initializer)
|
|
563
|
+
return;
|
|
564
|
+
const parsed = parseAttrDescriptorCall(d0.initializer, module, apiNames);
|
|
565
|
+
if (parsed.kind === "notMatch")
|
|
566
|
+
return;
|
|
567
|
+
if (parsed.kind === "error") {
|
|
568
|
+
diagnostics.push(parsed.diagnostic);
|
|
569
|
+
removedStatementIndices.add(i);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
descriptors.set(d0.name.name, parsed.value);
|
|
573
|
+
removedStatementIndices.add(i);
|
|
574
|
+
});
|
|
188
575
|
// Collect detected attribute markers
|
|
189
576
|
const markers = [];
|
|
190
|
-
const markerStatementIndices = new Set();
|
|
191
577
|
// Walk statements looking for attribute markers
|
|
192
578
|
module.body.forEach((stmt, i) => {
|
|
579
|
+
if (removedStatementIndices.has(i))
|
|
580
|
+
return;
|
|
193
581
|
if (stmt.kind !== "expressionStatement")
|
|
194
582
|
return;
|
|
195
583
|
const expr = stmt.expression;
|
|
196
584
|
if (expr.kind !== "call")
|
|
197
585
|
return;
|
|
198
|
-
const marker = tryDetectAttributeMarker(expr, module);
|
|
199
|
-
if (marker) {
|
|
200
|
-
markers.push(marker);
|
|
201
|
-
|
|
586
|
+
const marker = tryDetectAttributeMarker(expr, module, apiNames, descriptors);
|
|
587
|
+
if (marker.kind === "ok") {
|
|
588
|
+
markers.push(marker.value);
|
|
589
|
+
removedStatementIndices.add(i);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (marker.kind === "error") {
|
|
593
|
+
diagnostics.push(marker.diagnostic);
|
|
594
|
+
removedStatementIndices.add(i);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// If it looks like an attribute API call but doesn't match a supported marker,
|
|
598
|
+
// fail deterministically instead of leaving runtime-dead code in the output.
|
|
599
|
+
if (looksLikeAttributesApiUsage(expr, apiNames)) {
|
|
600
|
+
diagnostics.push(createDiagnostic("TSN4005", "error", `Invalid attribute marker call. Expected one of: A.on(X).type.add(...), A.on(X).ctor.add(...), A.on(X).method(x => x.m).add(...), A.on(X).prop(x => x.p).add(...)`, createLocation(module.filePath, expr.sourceSpan)));
|
|
601
|
+
removedStatementIndices.add(i);
|
|
202
602
|
}
|
|
203
603
|
});
|
|
204
|
-
// If
|
|
205
|
-
if (markers.length === 0) {
|
|
604
|
+
// If nothing to do, return module unchanged
|
|
605
|
+
if (markers.length === 0 && removedStatementIndices.size === 0) {
|
|
206
606
|
return module;
|
|
207
607
|
}
|
|
208
608
|
// Build map of declaration names to their indices
|
|
@@ -218,37 +618,89 @@ const processModule = (module, diagnostics) => {
|
|
|
218
618
|
});
|
|
219
619
|
// Build map of attributes per declaration
|
|
220
620
|
const classAttributes = new Map();
|
|
621
|
+
const classCtorAttributes = new Map();
|
|
622
|
+
const classMethodAttributes = new Map();
|
|
623
|
+
const classPropAttributes = new Map();
|
|
221
624
|
const functionAttributes = new Map();
|
|
222
625
|
for (const marker of markers) {
|
|
223
626
|
const classIndex = classDeclarations.get(marker.targetName);
|
|
224
627
|
const funcIndex = functionDeclarations.get(marker.targetName);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
628
|
+
const attr = {
|
|
629
|
+
kind: "attribute",
|
|
630
|
+
attributeType: marker.attributeType,
|
|
631
|
+
positionalArgs: marker.positionalArgs,
|
|
632
|
+
namedArgs: marker.namedArgs,
|
|
633
|
+
};
|
|
634
|
+
if (marker.targetSelector === "type") {
|
|
635
|
+
if (classIndex !== undefined && funcIndex !== undefined) {
|
|
636
|
+
diagnostics.push(createDiagnostic("TSN4005", "error", `Attribute target '${marker.targetName}' is ambiguous (matches both class and function)`, createLocation(module.filePath, marker.sourceSpan)));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (classIndex !== undefined) {
|
|
640
|
+
const attrs = classAttributes.get(classIndex) ?? [];
|
|
641
|
+
attrs.push(attr);
|
|
642
|
+
classAttributes.set(classIndex, attrs);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (funcIndex !== undefined) {
|
|
646
|
+
const attrs = functionAttributes.get(funcIndex) ?? [];
|
|
647
|
+
attrs.push(attr);
|
|
648
|
+
functionAttributes.set(funcIndex, attrs);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
diagnostics.push(createDiagnostic("TSN4007", "error", `Attribute target '${marker.targetName}' not found in module`, createLocation(module.filePath, marker.sourceSpan)));
|
|
652
|
+
continue;
|
|
246
653
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
654
|
+
if (classIndex === undefined) {
|
|
655
|
+
diagnostics.push(createDiagnostic("TSN4007", "error", `Attribute target '${marker.targetName}' not found in module`, createLocation(module.filePath, marker.sourceSpan)));
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const classStmt = module.body[classIndex];
|
|
659
|
+
if (marker.targetSelector === "ctor") {
|
|
660
|
+
const hasCtor = classStmt.members.some((m) => m.kind === "constructorDeclaration");
|
|
661
|
+
if (classStmt.isStruct && !hasCtor) {
|
|
662
|
+
diagnostics.push(createDiagnostic("TSN4005", "error", `Cannot apply constructor attributes to struct '${classStmt.name}' without an explicit constructor`, createLocation(module.filePath, marker.sourceSpan)));
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const attrs = classCtorAttributes.get(classIndex) ?? [];
|
|
666
|
+
attrs.push(attr);
|
|
667
|
+
classCtorAttributes.set(classIndex, attrs);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (marker.targetSelector === "method") {
|
|
671
|
+
const memberName = marker.selectedMemberName;
|
|
672
|
+
if (!memberName) {
|
|
673
|
+
diagnostics.push(createDiagnostic("TSN4005", "error", `Invalid attribute marker: method target missing member name`, createLocation(module.filePath, marker.sourceSpan)));
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const hasMember = classStmt.members.some((m) => m.kind === "methodDeclaration" && m.name === memberName);
|
|
677
|
+
if (!hasMember) {
|
|
678
|
+
diagnostics.push(createDiagnostic("TSN4007", "error", `Method '${classStmt.name}.${memberName}' not found for attribute target`, createLocation(module.filePath, marker.sourceSpan)));
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const perClass = classMethodAttributes.get(classIndex) ?? new Map();
|
|
682
|
+
const attrs = perClass.get(memberName) ?? [];
|
|
683
|
+
attrs.push(attr);
|
|
684
|
+
perClass.set(memberName, attrs);
|
|
685
|
+
classMethodAttributes.set(classIndex, perClass);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (marker.targetSelector === "prop") {
|
|
689
|
+
const memberName = marker.selectedMemberName;
|
|
690
|
+
if (!memberName) {
|
|
691
|
+
diagnostics.push(createDiagnostic("TSN4005", "error", `Invalid attribute marker: property target missing member name`, createLocation(module.filePath, marker.sourceSpan)));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const hasMember = classStmt.members.some((m) => m.kind === "propertyDeclaration" && m.name === memberName);
|
|
695
|
+
if (!hasMember) {
|
|
696
|
+
diagnostics.push(createDiagnostic("TSN4007", "error", `Property '${classStmt.name}.${memberName}' not found for attribute target`, createLocation(module.filePath, marker.sourceSpan)));
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const perClass = classPropAttributes.get(classIndex) ?? new Map();
|
|
700
|
+
const attrs = perClass.get(memberName) ?? [];
|
|
701
|
+
attrs.push(attr);
|
|
702
|
+
perClass.set(memberName, attrs);
|
|
703
|
+
classPropAttributes.set(classIndex, perClass);
|
|
252
704
|
}
|
|
253
705
|
}
|
|
254
706
|
// Rebuild module body:
|
|
@@ -257,20 +709,62 @@ const processModule = (module, diagnostics) => {
|
|
|
257
709
|
const newBody = [];
|
|
258
710
|
module.body.forEach((stmt, i) => {
|
|
259
711
|
// Skip marker statements
|
|
260
|
-
if (
|
|
712
|
+
if (removedStatementIndices.has(i))
|
|
261
713
|
return;
|
|
262
|
-
if (stmt.kind === "classDeclaration"
|
|
714
|
+
if (stmt.kind === "classDeclaration") {
|
|
263
715
|
// Update class with attributes
|
|
264
716
|
const classStmt = stmt;
|
|
265
717
|
const existingAttrs = classStmt.attributes ?? [];
|
|
266
|
-
const
|
|
267
|
-
|
|
718
|
+
const typeAttrs = classAttributes.get(i) ?? [];
|
|
719
|
+
const ctorAttrs = classCtorAttributes.get(i) ?? [];
|
|
720
|
+
const methodAttrs = classMethodAttributes.get(i);
|
|
721
|
+
const propAttrs = classPropAttributes.get(i);
|
|
722
|
+
const updatedMembers = methodAttrs || propAttrs
|
|
723
|
+
? classStmt.members.map((m) => {
|
|
724
|
+
if (m.kind === "methodDeclaration" && methodAttrs) {
|
|
725
|
+
const extras = methodAttrs.get(m.name);
|
|
726
|
+
if (extras && extras.length > 0) {
|
|
727
|
+
return {
|
|
728
|
+
...m,
|
|
729
|
+
attributes: [...(m.attributes ?? []), ...extras],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (m.kind === "propertyDeclaration" && propAttrs) {
|
|
734
|
+
const extras = propAttrs.get(m.name);
|
|
735
|
+
if (extras && extras.length > 0) {
|
|
736
|
+
return {
|
|
737
|
+
...m,
|
|
738
|
+
attributes: [...(m.attributes ?? []), ...extras],
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return m;
|
|
743
|
+
})
|
|
744
|
+
: classStmt.members;
|
|
745
|
+
const updated = {
|
|
268
746
|
...classStmt,
|
|
269
|
-
|
|
270
|
-
|
|
747
|
+
members: updatedMembers,
|
|
748
|
+
attributes: typeAttrs.length > 0
|
|
749
|
+
? [...existingAttrs, ...typeAttrs]
|
|
750
|
+
: classStmt.attributes,
|
|
751
|
+
ctorAttributes: ctorAttrs.length > 0
|
|
752
|
+
? [...(classStmt.ctorAttributes ?? []), ...ctorAttrs]
|
|
753
|
+
: classStmt.ctorAttributes,
|
|
754
|
+
};
|
|
755
|
+
// Avoid allocating new nodes when there are no changes.
|
|
756
|
+
if (typeAttrs.length === 0 &&
|
|
757
|
+
ctorAttrs.length === 0 &&
|
|
758
|
+
!methodAttrs &&
|
|
759
|
+
!propAttrs) {
|
|
760
|
+
newBody.push(classStmt);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
newBody.push(updated);
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
271
766
|
}
|
|
272
|
-
|
|
273
|
-
functionAttributes.has(i)) {
|
|
767
|
+
if (stmt.kind === "functionDeclaration" && functionAttributes.has(i)) {
|
|
274
768
|
// Update function with attributes
|
|
275
769
|
const funcStmt = stmt;
|
|
276
770
|
const existingAttrs = funcStmt.attributes ?? [];
|
|
@@ -279,11 +773,10 @@ const processModule = (module, diagnostics) => {
|
|
|
279
773
|
...funcStmt,
|
|
280
774
|
attributes: [...existingAttrs, ...newAttrs],
|
|
281
775
|
});
|
|
776
|
+
return;
|
|
282
777
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
newBody.push(stmt);
|
|
286
|
-
}
|
|
778
|
+
// Keep statement unchanged
|
|
779
|
+
newBody.push(stmt);
|
|
287
780
|
});
|
|
288
781
|
return {
|
|
289
782
|
...module,
|