@geonosis/oxlint-plugin-biological-architecture 2.8.0 → 2.10.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/CHANGELOG.md +28 -0
- package/corpus/tissues/bad-handler-prop.tsx +7 -0
- package/corpus/tissues/bad-passthrough.tsx +4 -2
- package/corpus/tissues/bad-untyped-prop.tsx +7 -0
- package/corpus/tissues/good-data-props.tsx +13 -0
- package/corpus/tokens/bad-default-parameter-colour.tsx +11 -0
- package/dist/index.js +78 -91
- package/package.json +2 -2
- package/corpus/tissues/bad-data-props.tsx +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @geonosis/oxlint-plugin-biological-architecture
|
|
2
2
|
|
|
3
|
+
## 2.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4c240df: `tissue-no-data-props` forbids state through props, not data (#9).
|
|
8
|
+
|
|
9
|
+
A stateless component handed `collections` by its parent and rendering a carousel cell had no
|
|
10
|
+
tier: the tissue rule read a data prop as ownership. Data is not state — the route fetched it, or
|
|
11
|
+
the tissue above did. A tissue may now take any declared data type. What it may not be handed is a
|
|
12
|
+
handler or setter (a function type, `Dispatch`, `SetStateAction`) or an undeclared one (`any`,
|
|
13
|
+
`unknown`, `object`, `{}`): `any` was "unknown, therefore allowed", and is now reported.
|
|
14
|
+
|
|
15
|
+
If you migrate against this, re-tier any `cells/` file that only re-exports a non-cell first. A
|
|
16
|
+
parent that stays in `cells/` and imports such a shim through a bare `cells` barrel is silent today
|
|
17
|
+
only because the barrel is untraced (#4); when #4's barrel fix lands it picks up
|
|
18
|
+
`cell-must-not-compose-cell`. A parent this release lets you move to `tissues/` is not affected by
|
|
19
|
+
that fix — it would only surface if barrels were followed through their re-exports.
|
|
20
|
+
|
|
21
|
+
## 2.9.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- e35392e: `tokens-only-colors` sees a colour that arrives as a default.
|
|
26
|
+
|
|
27
|
+
`({ color = '#090909' })` is never written in a JSX attribute, so the rule's only visitor could not
|
|
28
|
+
see it: one kit carried the same hex in 72 components, under this rule, at `"error"`, for its whole
|
|
29
|
+
life. A bare word is still left alone — a default is as often `variant = 'primary'` as a colour.
|
|
30
|
+
|
|
3
31
|
## 2.8.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Breaches tissue-no-data-props: a handler is state coordinated through props. The parent that
|
|
2
|
+
// holds `onSelect` holds state, which makes it a cell — and a cell does not render a tissue.
|
|
3
|
+
import { GoodStateful } from '../cells/good-stateful'
|
|
4
|
+
|
|
5
|
+
export function BadHandlerProp({ onSelect }: { onSelect: (id: string) => void }) {
|
|
6
|
+
return <GoodStateful key={String(onSelect)} />
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Breaches tissue-no-data-props: `any` is not a declaration. The tissue hands the value to a
|
|
2
|
+
// typed child; say what it is.
|
|
3
|
+
import { GoodStateful } from '../cells/good-stateful'
|
|
4
|
+
|
|
5
|
+
export function BadUntypedProp({ order }: { order: any }) {
|
|
6
|
+
return <GoodStateful key={String(order)} />
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The shape issue #9 had no tier for: stateless, handed rows by its parent, arranging a cell.
|
|
2
|
+
// Data is not state — a tissue may carry what it hands down.
|
|
3
|
+
import { GoodStateful } from '../cells/good-stateful'
|
|
4
|
+
|
|
5
|
+
export function GoodDataProps({ rows, title }: { rows: { id: string }[]; title: string }) {
|
|
6
|
+
return (
|
|
7
|
+
<section aria-label={title}>
|
|
8
|
+
{rows.map((row) => (
|
|
9
|
+
<GoodStateful key={row.id} />
|
|
10
|
+
))}
|
|
11
|
+
</section>
|
|
12
|
+
)
|
|
13
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -542,8 +542,8 @@ down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. A pare
|
|
|
542
542
|
hosts a cell (a summary card that opens a modal-hosted form) is two cells side by side: the child
|
|
543
543
|
hosts its own modal and reads whether it is open from the store, the parent writes it, and a
|
|
544
544
|
\`tissues/\` file arranges both. Cells coordinate through a store, never through props from one to
|
|
545
|
-
the other. A stateless adapter between cells is a tissue \u2014 it may take
|
|
546
|
-
|
|
545
|
+
the other. A stateless adapter between cells is a tissue \u2014 it may take the data it hands down
|
|
546
|
+
(\`{ collections: Collection[] }\`); it may not take a handler. Private siblings of the SAME
|
|
547
547
|
component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine. The
|
|
548
548
|
barrels \`cells/index.ts\`, \`cells/dynamic.ts\` and \`cells/dynamic.tsx\` are exempt \u2014 re-exporting
|
|
549
549
|
siblings is the shape \`cells-folder-index-is-barrel\` requires of them.`,
|
|
@@ -6286,35 +6286,37 @@ var ROUTE_FALLBACK_BASENAMES2 = /* @__PURE__ */ new Set([
|
|
|
6286
6286
|
"loading.tsx",
|
|
6287
6287
|
"not-found.tsx"
|
|
6288
6288
|
]);
|
|
6289
|
-
var
|
|
6290
|
-
var
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
"
|
|
6294
|
-
"
|
|
6295
|
-
"
|
|
6296
|
-
"ReactNode"
|
|
6297
|
-
]);
|
|
6298
|
-
var isKindVocabulary = (name) => /(Kind|EntityName)$/.test(name);
|
|
6299
|
-
function isAllowedTypeRefName(name) {
|
|
6300
|
-
if (REACT_NODE_TYPE_NAMES.has(name)) return true;
|
|
6301
|
-
if (isKindVocabulary(name)) return true;
|
|
6302
|
-
if (name.endsWith("Icon")) return true;
|
|
6303
|
-
if (name.endsWith("IconComponent")) return true;
|
|
6304
|
-
if (name.endsWith("IconType")) return true;
|
|
6289
|
+
var HANDLER_TYPE_NAMES = /* @__PURE__ */ new Set(["Dispatch", "Function", "SetStateAction"]);
|
|
6290
|
+
var UNDECLARED_KEYWORDS = /* @__PURE__ */ new Set(["TSAnyKeyword", "TSObjectKeyword", "TSUnknownKeyword"]);
|
|
6291
|
+
var isHandler = (t) => {
|
|
6292
|
+
if (!t?.type) return false;
|
|
6293
|
+
if (t.type === "TSFunctionType") return true;
|
|
6294
|
+
if (t.type === "TSTypeReference") return HANDLER_TYPE_NAMES.has(t.typeName?.name ?? "");
|
|
6295
|
+
if (t.type === "TSUnionType") return (t.types ?? []).some(isHandler);
|
|
6305
6296
|
return false;
|
|
6306
|
-
}
|
|
6307
|
-
|
|
6308
|
-
if (!t
|
|
6297
|
+
};
|
|
6298
|
+
var isUndeclared = (t) => {
|
|
6299
|
+
if (!t?.type) return false;
|
|
6300
|
+
if (UNDECLARED_KEYWORDS.has(t.type)) return true;
|
|
6301
|
+
if (t.type === "TSTypeLiteral") return (t.members ?? []).length === 0;
|
|
6302
|
+
if (t.type === "TSUnionType") return (t.types ?? []).some(isUndeclared);
|
|
6303
|
+
return false;
|
|
6304
|
+
};
|
|
6305
|
+
var KEYWORD_NAMES = {
|
|
6306
|
+
TSAnyKeyword: "any",
|
|
6307
|
+
TSNullKeyword: "null",
|
|
6308
|
+
TSObjectKeyword: "object",
|
|
6309
|
+
TSUndefinedKeyword: "undefined",
|
|
6310
|
+
TSUnknownKeyword: "unknown"
|
|
6311
|
+
};
|
|
6312
|
+
var describeTypeAnnotation = (t) => {
|
|
6313
|
+
if (!t?.type) return "unknown";
|
|
6314
|
+
if (t.type in KEYWORD_NAMES) return KEYWORD_NAMES[t.type] ?? t.type;
|
|
6309
6315
|
switch (t.type) {
|
|
6310
|
-
case "
|
|
6311
|
-
return "
|
|
6312
|
-
case "
|
|
6313
|
-
return "
|
|
6314
|
-
case "TSBooleanKeyword":
|
|
6315
|
-
return "boolean";
|
|
6316
|
-
case "TSArrayType":
|
|
6317
|
-
return `Array<${describeTypeAnnotation(t.elementType)}>`;
|
|
6316
|
+
case "TSFunctionType":
|
|
6317
|
+
return "a function";
|
|
6318
|
+
case "TSTypeLiteral":
|
|
6319
|
+
return "{}";
|
|
6318
6320
|
case "TSTypeReference":
|
|
6319
6321
|
return t.typeName?.name ?? "(unknown-ref)";
|
|
6320
6322
|
case "TSUnionType":
|
|
@@ -6322,47 +6324,7 @@ function describeTypeAnnotation(t) {
|
|
|
6322
6324
|
default:
|
|
6323
6325
|
return t.type;
|
|
6324
6326
|
}
|
|
6325
|
-
}
|
|
6326
|
-
function isLiteralStringUnion(t) {
|
|
6327
|
-
if (t.type !== "TSUnionType") return false;
|
|
6328
|
-
const types = t.types ?? [];
|
|
6329
|
-
if (types.length === 0) return false;
|
|
6330
|
-
return types.every((x) => x.type === "TSLiteralType" && typeof x.literal?.value === "string");
|
|
6331
|
-
}
|
|
6332
|
-
var narrowsAKindVocabulary = (t) => {
|
|
6333
|
-
const name = t.typeName?.name ?? "";
|
|
6334
|
-
if (name !== "Exclude" && name !== "Extract") return false;
|
|
6335
|
-
const [subject] = (t.typeArguments ?? t.typeParameters)?.params ?? [];
|
|
6336
|
-
return subject?.type === "TSTypeReference" && isKindVocabulary(subject.typeName?.name ?? "");
|
|
6337
|
-
};
|
|
6338
|
-
function isAllowedType(t) {
|
|
6339
|
-
if (!t || !t.type) return true;
|
|
6340
|
-
switch (t.type) {
|
|
6341
|
-
case "TSTypeReference":
|
|
6342
|
-
return isAllowedTypeRefName(t.typeName?.name ?? "") || narrowsAKindVocabulary(t);
|
|
6343
|
-
case "TSUnionType":
|
|
6344
|
-
return isLiteralStringUnion(t);
|
|
6345
|
-
// TSTypeLiteral (inline object shape) — allowed because nested shapes
|
|
6346
|
-
// Are typically ReactNode records; if it turns out to contain data
|
|
6347
|
-
// Primitives, the outer check will miss them (v1 limitation).
|
|
6348
|
-
case "TSTypeLiteral":
|
|
6349
|
-
return true;
|
|
6350
|
-
// Primitives and arrays fall through to forbidden.
|
|
6351
|
-
case "TSStringKeyword":
|
|
6352
|
-
case "TSNumberKeyword":
|
|
6353
|
-
case "TSBooleanKeyword":
|
|
6354
|
-
case "TSArrayType":
|
|
6355
|
-
return false;
|
|
6356
|
-
default:
|
|
6357
|
-
return true;
|
|
6358
|
-
}
|
|
6359
|
-
}
|
|
6360
|
-
var literalDefaultsOf = (param) => new Map(
|
|
6361
|
-
(param.properties ?? []).flatMap(
|
|
6362
|
-
(property) => property.type === "Property" && property.key?.type === "Identifier" && property.value?.type === "AssignmentPattern" && property.value.right?.type === "Literal" ? [[property.key.name ?? "", property.value.right.value]] : []
|
|
6363
|
-
)
|
|
6364
|
-
);
|
|
6365
|
-
var isLayoutConfiguration = (t, defaults, prop) => t.type === "TSNumberKeyword" && typeof defaults.get(prop) === "number" || t.type === "TSBooleanKeyword" && typeof defaults.get(prop) === "boolean";
|
|
6327
|
+
};
|
|
6366
6328
|
var tissueNoDataProps = {
|
|
6367
6329
|
create(context) {
|
|
6368
6330
|
const normalized = normalise(context.filename);
|
|
@@ -6377,20 +6339,17 @@ var tissueNoDataProps = {
|
|
|
6377
6339
|
if (!firstParam) return;
|
|
6378
6340
|
const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
|
|
6379
6341
|
if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
|
|
6380
|
-
const
|
|
6381
|
-
const defaults = literalDefaultsOf(firstParam);
|
|
6382
|
-
for (const member of members) {
|
|
6342
|
+
for (const member of typeLiteral.members ?? []) {
|
|
6383
6343
|
if (member.key?.type !== "Identifier") continue;
|
|
6384
|
-
const
|
|
6385
|
-
if (!
|
|
6386
|
-
if (ALWAYS_ALLOWED_PROP_NAMES.has(propName)) continue;
|
|
6344
|
+
const prop = member.key.name ?? "";
|
|
6345
|
+
if (!prop) continue;
|
|
6387
6346
|
const t = member.typeAnnotation?.typeAnnotation;
|
|
6388
6347
|
if (!t) continue;
|
|
6389
|
-
|
|
6390
|
-
if (
|
|
6348
|
+
const messageId = isHandler(t) ? "handler" : isUndeclared(t) ? "undeclared" : void 0;
|
|
6349
|
+
if (messageId === void 0) continue;
|
|
6391
6350
|
context.report({
|
|
6392
|
-
data: { prop
|
|
6393
|
-
messageId
|
|
6351
|
+
data: { prop, type: describeTypeAnnotation(t) },
|
|
6352
|
+
messageId,
|
|
6394
6353
|
node: member
|
|
6395
6354
|
});
|
|
6396
6355
|
}
|
|
@@ -6412,10 +6371,8 @@ var tissueNoDataProps = {
|
|
|
6412
6371
|
}
|
|
6413
6372
|
}
|
|
6414
6373
|
return {
|
|
6415
|
-
// Only
|
|
6416
|
-
//
|
|
6417
|
-
// Implementation details; their props don't define the tissue's
|
|
6418
|
-
// Contract, so they're out of scope for this rule.
|
|
6374
|
+
// Only the PUBLIC INTERFACE of the tissue file — the default or named export. Inner helpers
|
|
6375
|
+
// are file-scoped implementation; their props do not define the tissue's contract.
|
|
6419
6376
|
ExportDefaultDeclaration(node) {
|
|
6420
6377
|
checkDeclaration(node.declaration);
|
|
6421
6378
|
},
|
|
@@ -6424,17 +6381,21 @@ var tissueNoDataProps = {
|
|
|
6424
6381
|
}
|
|
6425
6382
|
};
|
|
6426
6383
|
},
|
|
6427
|
-
fixShape: `A tissue
|
|
6428
|
-
|
|
6429
|
-
\`
|
|
6430
|
-
|
|
6431
|
-
|
|
6384
|
+
fixShape: `A tissue holds no state, and data is not state: a tissue may take the rows, the cart or the title
|
|
6385
|
+
its parent hands it and hand them down to the cells and compounds it arranges (\`{ collections:
|
|
6386
|
+
Collection[] }\` rendering a carousel cell is a tissue). What a tissue is never handed is STATE \u2014 a
|
|
6387
|
+
handler or a setter (\`onSelect: (id) => void\`, \`setOpen: Dispatch<\u2026>\`) is coordination through
|
|
6388
|
+
props, and the parent holding it is a cell, which does not render tissues: move the handler into the
|
|
6389
|
+
cell below, or the file to \`cells/\`. And never an undeclared type \u2014 \`any\`, \`unknown\`, \`object\`,
|
|
6390
|
+
\`{}\`, alone or in a union \u2014 declare what the child takes. A tissue with no hooks, no store and no
|
|
6391
|
+
organelle that composes a cell and takes data is a tissue; do not invent a store to make it a cell.`,
|
|
6432
6392
|
meta: {
|
|
6433
6393
|
docs: {
|
|
6434
|
-
description: "
|
|
6394
|
+
description: "A tissue may carry data, never state. Its props may not include a handler or setter (a function type, `Dispatch`, `SetStateAction`, `Function`, or a union holding one) \u2014 that is state coordinated through props, which makes the parent a cell \u2014 nor an undeclared type (`any`, `unknown`, `object`, `{}`, or a union holding one). Every declared data type is allowed: the route fetched it or the tissue above did, and the tissue hands it down (#9)."
|
|
6435
6395
|
},
|
|
6436
6396
|
messages: {
|
|
6437
|
-
|
|
6397
|
+
handler: "Tissue prop '{{prop}}' is {{type}}. A handler or setter is state coordinated through props \u2014 the parent that holds it is a cell, and a cell does not render a tissue. Move the handler into the cell this tissue arranges, or move this file to cells/.",
|
|
6398
|
+
undeclared: "Tissue prop '{{prop}}' is typed '{{type}}', which declares nothing. A tissue hands data to typed children; declare the type the child takes. `any` is not a way past the tier wall."
|
|
6438
6399
|
},
|
|
6439
6400
|
schema: [],
|
|
6440
6401
|
type: "problem"
|
|
@@ -6866,6 +6827,10 @@ var COLOR_UTILITIES = /* @__PURE__ */ new Set([
|
|
|
6866
6827
|
var isColorUtility = (utility) => COLOR_UTILITIES.has(utility.replace(/^.*:/, ""));
|
|
6867
6828
|
|
|
6868
6829
|
// src/rules/tokens-only-colors.ts
|
|
6830
|
+
var defaultedName = (node) => {
|
|
6831
|
+
const left = node.left;
|
|
6832
|
+
return left?.type === "Identifier" ? left.name ?? "default" : "default";
|
|
6833
|
+
};
|
|
6869
6834
|
var tokenOffence = (value, prefixes, local) => {
|
|
6870
6835
|
const variable = variableOf(value);
|
|
6871
6836
|
if (variable !== void 0) {
|
|
@@ -6906,6 +6871,28 @@ var tokensOnlyColors = {
|
|
|
6906
6871
|
report("colorLiteral", { property: utility, value: inner });
|
|
6907
6872
|
}
|
|
6908
6873
|
}
|
|
6874
|
+
},
|
|
6875
|
+
/**
|
|
6876
|
+
* A colour that arrives as a DEFAULT — `({ color = '#090909' })` — is never written in a JSX
|
|
6877
|
+
* attribute, so the visitor above cannot see it. It reaches the element through a prop and
|
|
6878
|
+
* paints in the brand it was typed for, from the one place nobody looked: seventy-two icons
|
|
6879
|
+
* in one kit carried the same hex this way, under this rule, at "error", for its whole life.
|
|
6880
|
+
* A default is a value the component CHOOSES, which is exactly what a token is for.
|
|
6881
|
+
*
|
|
6882
|
+
* A bare word is left alone. `variant = 'primary'` and `size = 'small'` are defaults too, and
|
|
6883
|
+
* a rule that read every English word as a colour would be turned off within the week.
|
|
6884
|
+
*/
|
|
6885
|
+
AssignmentPattern(node) {
|
|
6886
|
+
const right = node.right;
|
|
6887
|
+
if (right?.type !== "Literal" || typeof right.value !== "string") return;
|
|
6888
|
+
const offence = tokenOffence(right.value, prefixes, /* @__PURE__ */ new Set());
|
|
6889
|
+
if (offence === void 0 || offence === "colorLiteral" && isNamedOnly(right.value))
|
|
6890
|
+
return;
|
|
6891
|
+
context.report({
|
|
6892
|
+
data: { property: defaultedName(node), value: right.value },
|
|
6893
|
+
messageId: offence,
|
|
6894
|
+
node
|
|
6895
|
+
});
|
|
6909
6896
|
}
|
|
6910
6897
|
};
|
|
6911
6898
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/oxlint-plugin-biological-architecture",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"oxlint": ">=1.77"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@geonosis/lint-parity": "2.
|
|
48
|
+
"@geonosis/lint-parity": "2.10.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=22"
|