modality-ts 0.0.6 → 0.0.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extraction/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAA6B,KAAK,cAAc,EAA4C,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC9K,OAAO,KAAK,EAAmB,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAErG,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAClE,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAuB,SAAQ,wBAAwB;IACtE,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAqND,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,SAAS,EAAE,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAa,GAAG,cAAc,CAuBhJ;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,wBAAwB,CAEzH;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,sBAAsB,CA2J3H"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extraction/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAA4C,KAAK,cAAc,EAA4C,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC7L,OAAO,KAAK,EAAmB,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAErG,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAClE,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAuB,SAAQ,wBAAwB;IACtE,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AA6OD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,SAAS,EAAE,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAa,GAAG,cAAc,CAuBhJ;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,wBAAwB,CAEzH;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,sBAAsB,CA4J3H"}
@@ -1,5 +1,5 @@
1
1
  import * as ts from "typescript";
2
- import { effectReads, effectWrites } from "modality-ts/kernel";
2
+ import { effectReads, effectWrites, validateValue } from "modality-ts/kernel";
3
3
  export * from "./pipeline/index.js";
4
4
  export * from "./spi/index.js";
5
5
  function emptyContextBindings() {
@@ -15,6 +15,29 @@ function setterBindingFromDecl(decl) {
15
15
  domain: decl.domain
16
16
  };
17
17
  }
18
+ function bindSetter(setters, symbolName, setter) {
19
+ setters.set(scopedSetterKey(setter.component, symbolName), setter);
20
+ const current = setters.get(symbolName);
21
+ if (!current || current.varId === setter.varId) {
22
+ setters.set(symbolName, setter);
23
+ return;
24
+ }
25
+ setters.delete(symbolName);
26
+ }
27
+ function scopedSetterKey(component, symbolName) {
28
+ return `${component}:${symbolName}`;
29
+ }
30
+ function settersForComponent(setters, component) {
31
+ if (!component)
32
+ return new Map(setters);
33
+ const scoped = new Map(setters);
34
+ for (const [key, setter] of setters) {
35
+ if (!key.startsWith(`${component}:`))
36
+ continue;
37
+ scoped.set(key.slice(component.length + 1), setter);
38
+ }
39
+ return scoped;
40
+ }
18
41
  function discoverContextBindings(source, fileName, route, typeAliases) {
19
42
  const bindings = emptyContextBindings();
20
43
  const providerValues = new Map();
@@ -246,7 +269,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
246
269
  const decl = options.stateVars.find((candidate) => candidate.id === channel.varId);
247
270
  if (!decl)
248
271
  continue;
249
- setters.set(channel.symbolName, setterBindingFromDecl(decl));
272
+ bindSetter(setters, channel.symbolName, setterBindingFromDecl(decl));
250
273
  }
251
274
  }
252
275
  for (const decl of contextBindings.vars) {
@@ -309,7 +332,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
309
332
  }
310
333
  if (setterName && ts.isBindingElement(setterName) && ts.isIdentifier(setterName.name)) {
311
334
  if (!options.writeChannels)
312
- setters.set(setterName.name.text, { varId, component, stateName: stateName.name.text, domain });
335
+ bindSetter(setters, setterName.name.text, { varId, component, stateName: stateName.name.text, domain });
313
336
  }
314
337
  }
315
338
  else {
@@ -319,7 +342,8 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
319
342
  const link = linkNavigationTransition(source, fileName, node, nextComponent ?? "Anonymous", routePatterns);
320
343
  if (link)
321
344
  transitions.push(link);
322
- const refTaint = refSetterTaint(node, setters);
345
+ const scopedSetters = settersForComponent(setters, nextComponent);
346
+ const refTaint = refSetterTaint(node, scopedSetters);
323
347
  if (refTaint) {
324
348
  const key = `Global taint ${refTaint.varId}`;
325
349
  if (!globalTaints.has(key)) {
@@ -327,8 +351,8 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
327
351
  warnings.push({ message: key, ...lineAndColumn(source, refTaint.node) });
328
352
  }
329
353
  }
330
- transitions.push(...transitionsFromTimerCall(source, fileName, node, setters, nextComponent ?? "Anonymous"));
331
- for (const timerTaint of timerSetterTaints(node, setters)) {
354
+ transitions.push(...transitionsFromTimerCall(source, fileName, node, scopedSetters, nextComponent ?? "Anonymous"));
355
+ for (const timerTaint of timerSetterTaints(node, scopedSetters)) {
332
356
  const key = `Global taint ${timerTaint.varId}`;
333
357
  if (!globalTaints.has(key)) {
334
358
  globalTaints.add(key);
@@ -336,7 +360,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
336
360
  }
337
361
  }
338
362
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isForwardablePropName(node.name.text) && !isIntrinsicJsxAttribute(node)) {
339
- const extracted = transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, warnings);
363
+ const extracted = transitionsFromComponentPropAttribute(source, fileName, node, scopedSetters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, warnings);
340
364
  transitions.push(...extracted);
341
365
  if (extracted.length === 0) {
342
366
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -346,7 +370,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
346
370
  const listInfo = listRenderedHandlerInfo(node, vars, nextComponent ?? "Anonymous");
347
371
  if (listInfo) {
348
372
  if (listInfo.domain.kind === "boundedList") {
349
- const extracted = transitionsFromBoundedListAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", {
373
+ const extracted = transitionsFromBoundedListAttribute(source, fileName, node, scopedSetters, handlers, nextComponent ?? "Anonymous", {
350
374
  varId: listInfo.varId,
351
375
  domain: listInfo.domain,
352
376
  itemName: listInfo.itemName
@@ -361,21 +385,21 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
361
385
  ts.forEachChild(node, (child) => visit(child, nextComponent));
362
386
  return;
363
387
  }
364
- const guardLocals = componentGuardLocalsFor(node, setters);
388
+ const guardLocals = componentGuardLocalsFor(node, scopedSetters);
365
389
  const guard = combineParsedGuards([
366
- renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
367
- disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
390
+ renderGuardFor(node, scopedSetters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
391
+ disabledGuardFor(node, scopedSetters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
368
392
  ]);
369
- const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, routePatterns, contextBindings, warnings);
393
+ const extracted = transitionsFromJsxAttribute(source, fileName, node, scopedSetters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, routePatterns, contextBindings, warnings);
370
394
  transitions.push(...extracted);
371
- if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, setters)) {
395
+ if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, scopedSetters)) {
372
396
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
373
397
  }
374
398
  }
375
399
  if (ts.isCallExpression(node) && isUseEffectCall(node)) {
376
- const extracted = transitionsFromUseEffect(source, fileName, node, setters, nextComponent ?? "Anonymous");
400
+ const extracted = transitionsFromUseEffect(source, fileName, node, scopedSetters, nextComponent ?? "Anonymous");
377
401
  transitions.push(...extracted);
378
- if (extracted.length === 0 && useEffectWritesModeledState(node, setters) && !providerComponents.has(nextComponent ?? "")) {
402
+ if (extracted.length === 0 && useEffectWritesModeledState(node, scopedSetters) && !providerComponents.has(nextComponent ?? "")) {
379
403
  warnings.push({ message: `Unextractable effect ${nextComponent ?? "Anonymous"}.useEffect`, ...lineAndColumn(source, node) });
380
404
  }
381
405
  }
@@ -451,23 +475,23 @@ function initialValueForUseState(call, domain) {
451
475
  if (parsed !== undefined)
452
476
  return parsed;
453
477
  if (initial.kind === ts.SyntaxKind.TrueKeyword)
454
- return true;
478
+ return validInitialOrFirst(domain, true);
455
479
  if (initial.kind === ts.SyntaxKind.FalseKeyword)
456
- return false;
480
+ return validInitialOrFirst(domain, false);
457
481
  if (ts.isStringLiteral(initial))
458
- return initial.text;
482
+ return validInitialOrFirst(domain, initial.text);
459
483
  if (ts.isNumericLiteral(initial))
460
- return Number(initial.text);
484
+ return validInitialOrFirst(domain, Number(initial.text));
461
485
  if (initial.kind === ts.SyntaxKind.NullKeyword)
462
- return null;
486
+ return validInitialOrFirst(domain, null);
463
487
  if (ts.isArrayLiteralExpression(initial))
464
- return initial.elements.length === 0 ? "0" : initial.elements.length === 1 ? "1" : "many";
488
+ return validInitialOrFirst(domain, initial.elements.length === 0 ? "0" : initial.elements.length === 1 ? "1" : "many");
465
489
  return firstValue(domain);
466
490
  }
467
491
  function initialValueFromExpression(expression, domain) {
468
492
  const literal = literalValue(expression);
469
493
  if (literal !== undefined)
470
- return literal;
494
+ return validateValue(domain, literal) ? literal : undefined;
471
495
  if (domain.kind === "option")
472
496
  return initialValueFromExpression(expression, domain.inner);
473
497
  if (domain.kind === "record" && ts.isObjectLiteralExpression(expression)) {
@@ -480,6 +504,9 @@ function initialValueFromExpression(expression, domain) {
480
504
  }
481
505
  return undefined;
482
506
  }
507
+ function validInitialOrFirst(domain, value) {
508
+ return validateValue(domain, value) ? value : firstValue(domain);
509
+ }
483
510
  function domainFromLiteralType(node) {
484
511
  const lit = node.literal;
485
512
  if (lit.kind === ts.SyntaxKind.TrueKeyword || lit.kind === ts.SyntaxKind.FalseKeyword)
@@ -2285,7 +2312,9 @@ function transitionsFromUseEffect(source, fileName, node, setters, component) {
2285
2312
  const effectReads = uniqueStrings(summaries.flatMap((summary) => summary.reads));
2286
2313
  const deps = dependencyReads(node.arguments[1], setters, effectReads);
2287
2314
  const effects = summaries.map((summary) => summary.effect);
2288
- if (effects.length > 0) {
2315
+ const hasExplicitDependencyArray = Boolean(node.arguments[1] && ts.isArrayLiteralExpression(node.arguments[1]));
2316
+ const hasUntriggeredHavocEffect = hasExplicitDependencyArray && deps.length === 0 && effects.some((effect) => effect.kind === "havoc");
2317
+ if (effects.length > 0 && !hasUntriggeredHavocEffect) {
2289
2318
  const assignEffects = effects.filter((effect) => effect.kind === "assign");
2290
2319
  const guards = assignEffects.map((effect) => ({ kind: "neq", args: [{ kind: "read", var: effect.var }, effect.expr] }));
2291
2320
  const guard = guards.length > 0 ? guards.slice(1).reduce((acc, next) => andGuard(acc, next), guards[0]) : { kind: "lit", value: true };