@jskit-ai/config-eslint 0.1.242 → 0.1.244

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/config-eslint",
3
- "version": "0.1.242",
3
+ "version": "0.1.244",
4
4
  "description": "Shared flat ESLint presets for JSKIT projects.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,4 +1,4 @@
1
- import { createReporter, isActionExecute, keyName, ruleMeta } from "../support.js";
1
+ import { createReporter, isActionExecute, keyName, ruleMeta, unwrapAwait, unwrapChain } from "../support.js";
2
2
 
3
3
  // A callee a reader follows in one step: a name or a member chain, with no
4
4
  // call inside it.
@@ -9,11 +9,6 @@ function isPlainCallee(node) {
9
9
  return node.type === "MemberExpression" && isPlainCallee(node.object);
10
10
  }
11
11
 
12
- function unwrapCall(node) {
13
- const value = node.type === "AwaitExpression" ? node.argument : node;
14
- return value.type === "ChainExpression" ? value.expression : value;
15
- }
16
-
17
12
  // The shape problem of an action's execute, or null for
18
13
  // `execute: (input, context) => service.method(…)`. Its parameters may
19
14
  // destructure what must not reach the service (`({ workspaceSlug, ...payload },
@@ -34,7 +29,7 @@ function shapeProblem(value) {
34
29
  if (value.type !== "ArrowFunctionExpression") {
35
30
  return "notArrow";
36
31
  }
37
- const body = unwrapCall(value.body);
32
+ const body = unwrapChain(unwrapAwait(value.body));
38
33
  if (body.type !== "CallExpression" || !isPlainCallee(body.callee)) {
39
34
  return "notOneCall";
40
35
  }
@@ -1,4 +1,4 @@
1
- import { ABSENT_DEFAULT, CLIENT_GUIDE_URL, compactText, createReporter, keyName, ruleMeta } from "../support.js";
1
+ import { ABSENT_DEFAULT, CLIENT_GUIDE_URL, compactText, createReporter, keyName, ruleMeta, unwrapChain } from "../support.js";
2
2
 
3
3
  // Prop types whose values are data: a call on one (`items.map()`) is not a call on a collaborator.
4
4
  const DATA_TYPES = new Set(["Array", "String", "Number", "Boolean", "Date", "RegExp", "Symbol", "BigInt"]);
@@ -21,26 +21,22 @@ function declaredProps(node) {
21
21
  return null;
22
22
  }
23
23
 
24
- function unchain(node) {
25
- return node?.type === "ChainExpression" ? node.expression : node;
26
- }
27
-
28
24
  // The prop a call reaches through, or "": `props.x()`, `props.x.y()` or `props.x?.y()` in the script, `x()` or
29
25
  // `x.y()` in the template.
30
26
  function calledProp(callee, propsBinding, inTemplate) {
31
- const target = unchain(callee);
27
+ const target = unwrapChain(callee);
32
28
  if (inTemplate) {
33
- const root = target.type === "MemberExpression" ? unchain(target.object) : target;
29
+ const root = target.type === "MemberExpression" ? unwrapChain(target.object) : target;
34
30
  return root.type === "Identifier" ? root.name : "";
35
31
  }
36
32
  if (target.type !== "MemberExpression") {
37
33
  return "";
38
34
  }
39
- const owner = unchain(target.object);
35
+ const owner = unwrapChain(target.object);
40
36
  if (owner.type === "Identifier" && owner.name === propsBinding) {
41
37
  return target.computed ? "" : target.property.name;
42
38
  }
43
- const root = owner.type === "MemberExpression" && !owner.computed ? unchain(owner.object) : null;
39
+ const root = owner.type === "MemberExpression" && !owner.computed ? unwrapChain(owner.object) : null;
44
40
  return root?.type === "Identifier" && root.name === propsBinding ? owner.property.name : "";
45
41
  }
46
42
 
@@ -1,5 +1,5 @@
1
1
  import { FUNCTION_TYPES, definedName, definitionOf, enclosingFunction } from "../factoryResults.js";
2
- import { CLIENT_GUIDE_URL, createReporter, keyName, ruleMeta } from "../support.js";
2
+ import { CLIENT_GUIDE_URL, createReporter, keyName, ruleMeta, unwrapAwait } from "../support.js";
3
3
 
4
4
  const RESULT_FACTORY = /^(create|use)[A-Z]/u;
5
5
 
@@ -25,10 +25,6 @@ function addsBehaviour(sourceCode, value) {
25
25
  return definition?.type === "FunctionName" || (definition?.type === "Variable" && FUNCTION_TYPES.has(definition.node.init?.type));
26
26
  }
27
27
 
28
- function unwrapAwait(node) {
29
- return node?.type === "AwaitExpression" ? node.argument : node;
30
- }
31
-
32
28
  // The create*/use* call a wrapper's callback builds its result with, or null:
33
29
  // `app.runWithContext(() => createRuntime(readers))` returns that runtime.
34
30
  function callbackFactoryCall(call) {
@@ -1,11 +1,7 @@
1
- import { createReporter, ruleMeta } from "../support.js";
1
+ import { createReporter, ruleMeta, unwrapChain } from "../support.js";
2
2
 
3
3
  const REFERENCE_TYPES = new Set(["Identifier", "MemberExpression", "ThisExpression"]);
4
4
 
5
- function unwrapChain(node) {
6
- return node?.type === "ChainExpression" ? node.expression : node;
7
- }
8
-
9
5
  // A reference's text with optional links read as plain ones, so `a?.b` and
10
6
  // `a.b` name the same value.
11
7
  function referenceText(sourceCode, node) {
@@ -1,5 +1,5 @@
1
1
  import { CREATE_NAME, FUNCTION_TYPES, definedName, definitionOf, enclosingFunction, holdsBehaviour, localFunction, returnsOnlyData } from "../factoryResults.js";
2
- import { createReporter, isClientLint, isFreezeCall, ruleMeta } from "../support.js";
2
+ import { createReporter, isClientLint, isFreezeCall, ruleMeta, unwrapAwait } from "../support.js";
3
3
 
4
4
  // A decorator over the JSKIT CRUD service or repository spreads it: its
5
5
  // members are the contract's.
@@ -7,17 +7,13 @@ const CRUD_DECORATOR = /^decorate\w*(?:Service|Repository)$/u;
7
7
  // In client code a composable's result is an API too.
8
8
  const CLIENT_FACTORY_NAME = /^(create|use)[A-Z]/u;
9
9
 
10
- function unwrap(node) {
11
- return node?.type === "AwaitExpression" ? node.argument : node;
12
- }
13
-
14
10
  // The create* call a spread argument comes from: the call itself, or the call
15
11
  // that initialised the variable it reads.
16
12
  function factoryCall(sourceCode, argument) {
17
- const value = unwrap(argument);
13
+ const value = unwrapAwait(argument);
18
14
  if (value.type === "Identifier") {
19
15
  const definition = definitionOf(sourceCode, value);
20
- const init = unwrap(definition?.type === "Variable" ? definition.node.init : null);
16
+ const init = unwrapAwait(definition?.type === "Variable" ? definition.node.init : null);
21
17
  return init?.type === "CallExpression" ? { call: init, via: value.name } : null;
22
18
  }
23
19
  return value.type === "CallExpression" ? { call: value, via: "" } : null;
@@ -7,6 +7,7 @@ import {
7
7
  isFactory,
8
8
  receivingFunction,
9
9
  ruleMeta,
10
+ unwrapChain,
10
11
  walk
11
12
  } from "../support.js";
12
13
 
@@ -100,7 +101,7 @@ const requiredCollaborators = Object.freeze({
100
101
  }
101
102
  function checkProbe(node) {
102
103
  const probed = probedArgument(node);
103
- const argument = probed?.type === "ChainExpression" ? probed.expression : probed;
104
+ const argument = unwrapChain(probed);
104
105
  const receiver = argument?.type === "Identifier" ? receivingFunction(context, node, argument.name) : null;
105
106
  if (argument?.type === "MemberExpression" || (receiver && isFactory(receiver, client))) {
106
107
  report(node, "method-probe", "methodProbe", { probe: sourceCode.getText(probed) });
@@ -109,7 +110,7 @@ const requiredCollaborators = Object.freeze({
109
110
  // A template has no parameters of its own, so only member probes are read there.
110
111
  function checkTemplateProbe(node) {
111
112
  const probed = probedArgument(node);
112
- const argument = probed?.type === "ChainExpression" ? probed.expression : probed;
113
+ const argument = unwrapChain(probed);
113
114
  if (argument?.type === "MemberExpression") {
114
115
  report(node, "method-probe", "methodProbe", { probe: sourceCode.getText(probed) });
115
116
  }
@@ -33,16 +33,31 @@ function isTypedInjection(sourceCode, call) {
33
33
  return hasJsdocBefore(sourceCode, owner, RETURNS_TAG);
34
34
  }
35
35
 
36
+ /**
37
+ * The `required` option of the function holding an inject() call, as in `useRuntime({ required = false } = {})`.
38
+ * @param {any} sourceCode
39
+ * @param {any} call
40
+ */
41
+ function requiredOption(sourceCode, call) {
42
+ const first = sourceCode.getAncestors(call).findLast((ancestor) => FUNCTION_TYPES.has(ancestor.type))?.params[0];
43
+ const pattern = first?.type === "AssignmentPattern" ? first.left : first;
44
+ return pattern?.type === "ObjectPattern"
45
+ ? pattern.properties.find((property) => property.type === "Property" && property.key.name === "required")
46
+ : undefined;
47
+ }
48
+
36
49
  // An injected value is a capability lookup: it takes the owner's exported key,
37
50
  // names its type, and resolves once, either required (`inject(KEY)`) or to the
38
51
  // owner's named default (`inject(KEY, NO_X)`), never to null or {} that is then
39
- // probed. A consumer that cannot depend on the key's owner writes the key in
40
- // place, typed and with a named NO_* default, as a server token is written.
52
+ // probed, and never by a `required` mode that gives one key two outcomes. A
53
+ // consumer that cannot depend on the key's owner writes the key in place, typed
54
+ // and with a named NO_* default, as a server token is written.
41
55
  const typedInject = Object.freeze({
42
56
  meta: ruleMeta("Require inject() to take an exported key, name the injected type and never default to nothing.", {
43
57
  literalKey: "inject({{key}}) names its key in place. Import the owner's exported key constant.",
44
58
  absentDefault: "inject(…, {{fallback}}) resolves to nothing, so every use probes it. Inject it required, or default to the owner's named default.",
45
- untypedInject: "inject({{key}}) has no type. Name it with a JSDoc @type on the binding, a cast, or the accessor's @returns."
59
+ untypedInject: "inject({{key}}) has no type. Name it with a JSDoc @type on the binding, a cast, or the accessor's @returns.",
60
+ requiredMode: "A required option gives inject({{key}}) two outcomes. Resolve it one way; a caller that cannot work without the value refuses where it receives it."
46
61
  }, "injected-values", CLIENT_GUIDE_URL),
47
62
  create(context) {
48
63
  const sourceCode = context.sourceCode;
@@ -69,6 +84,10 @@ const typedInject = Object.freeze({
69
84
  if (!isTypedInjection(sourceCode, node)) {
70
85
  report(node, "untyped-inject", "untypedInject", { key: keyText });
71
86
  }
87
+ const required = requiredOption(sourceCode, node);
88
+ if (required) {
89
+ report(required, "inject-required-mode", "requiredMode", { key: keyText });
90
+ }
72
91
  }
73
92
  };
74
93
  }
package/plugin/support.js CHANGED
@@ -94,6 +94,16 @@ function keyName(property) {
94
94
  return property?.key?.name || property?.key?.value;
95
95
  }
96
96
 
97
+ // The expression an optional chain wraps, so `a?.b` reads as `a.b`; any other node as it is.
98
+ function unwrapChain(node) {
99
+ return node?.type === "ChainExpression" ? node.expression : node;
100
+ }
101
+
102
+ // The value an `await` waits for, so `await x` reads as `x`; any other node as it is.
103
+ function unwrapAwait(node) {
104
+ return node?.type === "AwaitExpression" ? node.argument : node;
105
+ }
106
+
97
107
  // The node a reader sees as the function: a method's property or class member,
98
108
  // otherwise the function itself.
99
109
  function functionSite(fn) {
@@ -373,5 +383,7 @@ export {
373
383
  keyName,
374
384
  receivingFunction,
375
385
  ruleMeta,
386
+ unwrapAwait,
387
+ unwrapChain,
376
388
  walk
377
389
  };