@ashstack/lint 0.1.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/LICENSE +21 -0
- package/README.md +60 -0
- package/RULES.md +2881 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +222 -0
- package/dist/core/rules/core/components-tsx-only.d.ts +2 -0
- package/dist/core/rules/core/components-tsx-only.js +37 -0
- package/dist/core/rules/core/hoist-intl.d.ts +2 -0
- package/dist/core/rules/core/hoist-intl.js +32 -0
- package/dist/core/rules/core/index.d.ts +2 -0
- package/dist/core/rules/core/index.js +18 -0
- package/dist/core/rules/core/no-comments.d.ts +2 -0
- package/dist/core/rules/core/no-comments.js +194 -0
- package/dist/core/rules/core/no-naming-convention.d.ts +2 -0
- package/dist/core/rules/core/no-naming-convention.js +143 -0
- package/dist/core/rules/core/use-design-system.d.ts +2 -0
- package/dist/core/rules/core/use-design-system.js +147 -0
- package/dist/core/rules/zod/index.d.ts +2 -0
- package/dist/core/rules/zod/index.js +12 -0
- package/dist/core/rules/zod/prefer-enum.d.ts +2 -0
- package/dist/core/rules/zod/prefer-enum.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/lib/ast.d.ts +44 -0
- package/dist/lib/ast.js +162 -0
- package/dist/lib/detect.d.ts +9 -0
- package/dist/lib/detect.js +42 -0
- package/dist/lib/merge.d.ts +9 -0
- package/dist/lib/merge.js +18 -0
- package/dist/lib/module.d.ts +19 -0
- package/dist/lib/module.js +49 -0
- package/dist/lib/types.d.ts +114 -0
- package/dist/lib/types.js +1 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +126 -0
- package/dist/react/rules/i18n/index.d.ts +2 -0
- package/dist/react/rules/i18n/index.js +16 -0
- package/dist/react/rules/i18n/no-bare-attrs.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-attrs.js +38 -0
- package/dist/react/rules/i18n/no-bare-text.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-text.js +26 -0
- package/dist/react/rules/i18n/no-bare-toast.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-toast.js +38 -0
- package/dist/react/rules/query/index.d.ts +2 -0
- package/dist/react/rules/query/index.js +20 -0
- package/dist/react/rules/query/next-page-param-undefined.d.ts +2 -0
- package/dist/react/rules/query/next-page-param-undefined.js +26 -0
- package/dist/react/rules/query/no-deprecated-filters.d.ts +2 -0
- package/dist/react/rules/query/no-deprecated-filters.js +63 -0
- package/dist/react/rules/query/no-fetch-in-query-fn.d.ts +2 -0
- package/dist/react/rules/query/no-fetch-in-query-fn.js +22 -0
- package/dist/react/rules/query/no-inline-keys.d.ts +2 -0
- package/dist/react/rules/query/no-inline-keys.js +107 -0
- package/dist/react/rules/query/require-destructured-hooks.d.ts +2 -0
- package/dist/react/rules/query/require-destructured-hooks.js +41 -0
- package/dist/react/rules/query/shared.d.ts +2 -0
- package/dist/react/rules/query/shared.js +10 -0
- package/dist/react/rules/zustand/index.d.ts +2 -0
- package/dist/react/rules/zustand/index.js +12 -0
- package/dist/react/rules/zustand/require-selector.d.ts +2 -0
- package/dist/react/rules/zustand/require-selector.js +62 -0
- package/dist/react-native/index.d.ts +11 -0
- package/dist/react-native/index.js +113 -0
- package/dist/react-native/rules/keyboard/index.d.ts +2 -0
- package/dist/react-native/rules/keyboard/index.js +18 -0
- package/dist/react-native/rules/legend-list/index.d.ts +2 -0
- package/dist/react-native/rules/legend-list/index.js +51 -0
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +77 -0
- package/dist/react-native/rules/legend-list/no-index-key-extractor.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +26 -0
- package/dist/react-native/rules/legend-list/no-inline-data.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-data.js +25 -0
- package/dist/react-native/rules/legend-list/no-inline-extra-data.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +21 -0
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +35 -0
- package/dist/react-native/rules/legend-list/no-mixed-children.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-mixed-children.js +24 -0
- package/dist/react-native/rules/legend-list/no-remount-key.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-remount-key.js +18 -0
- package/dist/react-native/rules/legend-list/no-scrollview-map.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +27 -0
- package/dist/react-native/rules/legend-list/no-unsupported-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +29 -0
- package/dist/react-native/rules/legend-list/required-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/required-props.js +23 -0
- package/dist/react-native/rules/legend-list/shared.d.ts +19 -0
- package/dist/react-native/rules/legend-list/shared.js +25 -0
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.d.ts +2 -0
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +49 -0
- package/dist/react-native/rules/legend-state/index.d.ts +2 -0
- package/dist/react-native/rules/legend-state/index.js +35 -0
- package/dist/react-native/rules/legend-state/naming.d.ts +2 -0
- package/dist/react-native/rules/legend-state/naming.js +26 -0
- package/dist/react-native/rules/legend-state/no-assignment.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-assignment.js +31 -0
- package/dist/react-native/rules/legend-state/no-nested-observable.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-nested-observable.js +23 -0
- package/dist/react-native/rules/legend-state/no-object-selector.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-object-selector.js +47 -0
- package/dist/react-native/rules/legend-state/no-observable-in-component.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +46 -0
- package/dist/react-native/rules/legend-state/no-peek-in-selector.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +67 -0
- package/dist/react-native/rules/legend-state/no-react-mirror.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-react-mirror.js +33 -0
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +52 -0
- package/dist/react-native/rules/legend-state/shared.d.ts +9 -0
- package/dist/react-native/rules/legend-state/shared.js +23 -0
- package/dist/react-native/rules/react-native/hoist-stateless-function.d.ts +2 -0
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +59 -0
- package/dist/react-native/rules/react-native/index.d.ts +2 -0
- package/dist/react-native/rules/react-native/index.js +50 -0
- package/dist/react-native/rules/react-native/no-conditional-style-array.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +23 -0
- package/dist/react-native/rules/react-native/no-dynamic-import.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-dynamic-import.js +18 -0
- package/dist/react-native/rules/react-native/no-keyboard-will-events.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +18 -0
- package/dist/react-native/rules/react-native/no-leaked-render.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-leaked-render.js +43 -0
- package/dist/react-native/rules/react-native/no-manual-memo.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-manual-memo.js +87 -0
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +36 -0
- package/dist/react-native/rules/react-native/no-rn-image-network-source.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +58 -0
- package/dist/react-native/rules/react-native/no-rn-namespace-import.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +38 -0
- package/dist/react-native/rules/react-native/no-scroll-position-state.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +26 -0
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +62 -0
- package/dist/react-native/rules/reanimated/animated-reaction-safety.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +110 -0
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +72 -0
- package/dist/react-native/rules/reanimated/animated-updater-purity.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +38 -0
- package/dist/react-native/rules/reanimated/gpu-properties-only.d.ts +2 -0
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +80 -0
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.d.ts +2 -0
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +43 -0
- package/dist/react-native/rules/reanimated/index.d.ts +2 -0
- package/dist/react-native/rules/reanimated/index.js +51 -0
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.d.ts +2 -0
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +41 -0
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.d.ts +2 -0
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +86 -0
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.d.ts +2 -0
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +112 -0
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.d.ts +2 -0
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +24 -0
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.d.ts +2 -0
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +21 -0
- package/dist/react-native/rules/reanimated/shared-value-usage.d.ts +2 -0
- package/dist/react-native/rules/reanimated/shared-value-usage.js +105 -0
- package/dist/react-native/rules/reanimated/shared.d.ts +5 -0
- package/dist/react-native/rules/reanimated/shared.js +13 -0
- package/dist/react-native/rules/skia/canvas-opaque.d.ts +2 -0
- package/dist/react-native/rules/skia/canvas-opaque.js +49 -0
- package/dist/react-native/rules/skia/index.d.ts +2 -0
- package/dist/react-native/rules/skia/index.js +21 -0
- package/dist/react-native/rules/turbo-image/index.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/index.js +14 -0
- package/dist/react-native/rules/turbo-image/require-cache-policy.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/require-cache-policy.js +21 -0
- package/dist/react-native/rules/turbo-image/require-resize.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/require-resize.js +22 -0
- package/dist/react-native/rules/unistyles/animated-theme.d.ts +2 -0
- package/dist/react-native/rules/unistyles/animated-theme.js +37 -0
- package/dist/react-native/rules/unistyles/content-container.d.ts +2 -0
- package/dist/react-native/rules/unistyles/content-container.js +73 -0
- package/dist/react-native/rules/unistyles/in-sheet.d.ts +2 -0
- package/dist/react-native/rules/unistyles/in-sheet.js +116 -0
- package/dist/react-native/rules/unistyles/index.d.ts +2 -0
- package/dist/react-native/rules/unistyles/index.js +58 -0
- package/dist/react-native/rules/unistyles/insets.d.ts +2 -0
- package/dist/react-native/rules/unistyles/insets.js +92 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-color.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +30 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +78 -0
- package/dist/react-native/rules/unistyles/no-margin.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-margin.js +19 -0
- package/dist/react-native/rules/unistyles/no-style-spread.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-style-spread.js +41 -0
- package/dist/react-native/rules/unistyles/no-unused-styles.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-unused-styles.js +99 -0
- package/dist/react-native/rules/unistyles/rtl-style-call.d.ts +2 -0
- package/dist/react-native/rules/unistyles/rtl-style-call.js +42 -0
- package/dist/react-native/rules/unistyles/shared.d.ts +16 -0
- package/dist/react-native/rules/unistyles/shared.js +70 -0
- package/dist/react-native/rules/unistyles/theme-screen-component.d.ts +2 -0
- package/dist/react-native/rules/unistyles/theme-screen-component.js +22 -0
- package/dist/react-native/rules/unistyles/theme-style-attr.d.ts +2 -0
- package/dist/react-native/rules/unistyles/theme-style-attr.js +18 -0
- package/dist/react-native/stylesheet.d.ts +11 -0
- package/dist/react-native/stylesheet.js +44 -0
- package/package.json +70 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { problem } from "../../../lib/ast.js";
|
|
2
|
+
const IDENTIFIER_NAME = /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u;
|
|
3
|
+
const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
|
|
4
|
+
const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/;
|
|
5
|
+
const CONSTANT_CASE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
|
|
6
|
+
const SNAKE_CASE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
|
|
7
|
+
const PLACEHOLDER_NAME = /^_+$/;
|
|
8
|
+
const FORMAT_TESTS = {
|
|
9
|
+
camelCase: name => CAMEL_CASE.test(name),
|
|
10
|
+
PascalCase: name => PASCAL_CASE.test(name) && /[a-z]/.test(name),
|
|
11
|
+
CONSTANT_CASE: name => CONSTANT_CASE.test(name),
|
|
12
|
+
snake_case: name => SNAKE_CASE.test(name),
|
|
13
|
+
};
|
|
14
|
+
const ANY_CASE = ["camelCase", "snake_case", "CONSTANT_CASE", "PascalCase"];
|
|
15
|
+
const LEGEND_STATE_OBSERVABLE_SUFFIX = String.raw `\$?`;
|
|
16
|
+
const AFFIXED_NAME = String.raw `[$_]*([A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)_*${LEGEND_STATE_OBSERVABLE_SUFFIX}`;
|
|
17
|
+
const NUMERIC_KEY = String.raw `(?:[0-9.]+)`;
|
|
18
|
+
const LIBRARY_PROPERTY_NAMES = [
|
|
19
|
+
"enableFullScreenImage_legacy",
|
|
20
|
+
"experimental_backgroundImage",
|
|
21
|
+
"unstable_conditionNames",
|
|
22
|
+
];
|
|
23
|
+
const LIBRARY_VARIABLE_NAMES = ["unstable_settings"];
|
|
24
|
+
const spelledExactly = (...alternatives) => new RegExp(`^(?:${alternatives.join("|")})$`);
|
|
25
|
+
const CONVENTIONS = {
|
|
26
|
+
objectLiteralMember: {
|
|
27
|
+
match: spelledExactly(...LIBRARY_PROPERTY_NAMES, NUMERIC_KEY, AFFIXED_NAME),
|
|
28
|
+
formats: ANY_CASE,
|
|
29
|
+
label: "object property",
|
|
30
|
+
},
|
|
31
|
+
typeMember: {
|
|
32
|
+
match: spelledExactly(AFFIXED_NAME),
|
|
33
|
+
formats: ANY_CASE,
|
|
34
|
+
label: "type member",
|
|
35
|
+
},
|
|
36
|
+
variable: {
|
|
37
|
+
match: spelledExactly(...LIBRARY_VARIABLE_NAMES, AFFIXED_NAME),
|
|
38
|
+
formats: ["camelCase", "CONSTANT_CASE", "PascalCase"],
|
|
39
|
+
label: "variable",
|
|
40
|
+
},
|
|
41
|
+
enumMember: {
|
|
42
|
+
match: null,
|
|
43
|
+
formats: ["CONSTANT_CASE", "PascalCase"],
|
|
44
|
+
label: "enum member",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const matchesFormat = (format, name) => FORMAT_TESTS[format]?.(name) === true;
|
|
48
|
+
const memberName = (key) => {
|
|
49
|
+
if (!key)
|
|
50
|
+
return null;
|
|
51
|
+
if (key.type === "Identifier")
|
|
52
|
+
return key.name;
|
|
53
|
+
if (key.type !== "Literal")
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof key.value === "string")
|
|
56
|
+
return IDENTIFIER_NAME.test(key.value) ? key.value : null;
|
|
57
|
+
if (typeof key.value === "number")
|
|
58
|
+
return key.raw ?? String(key.value);
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
const namingViolation = (name, kind) => {
|
|
62
|
+
const convention = CONVENTIONS[kind];
|
|
63
|
+
const wrongFormat = `Rename this ${convention.label} \`${name}\` to ${convention.formats.join(", ")}.`;
|
|
64
|
+
if (convention.match === null) {
|
|
65
|
+
return convention.formats.some(format => matchesFormat(format, name)) ? null : wrongFormat;
|
|
66
|
+
}
|
|
67
|
+
const matched = convention.match.exec(name);
|
|
68
|
+
if (!matched) {
|
|
69
|
+
return `Rename this ${convention.label} \`${name}\` to a plain ${convention.formats.join("/")} identifier, with at most a leading \`_\` or a trailing \`$\`.`;
|
|
70
|
+
}
|
|
71
|
+
const captured = matched[1];
|
|
72
|
+
if (!captured)
|
|
73
|
+
return null;
|
|
74
|
+
return convention.formats.some(format => matchesFormat(format, captured)) ? null : wrongFormat;
|
|
75
|
+
};
|
|
76
|
+
export const noNamingConvention = problem("Require a variable, object property, type member or enum member to use one of the casings allowed for its kind.", {
|
|
77
|
+
createOnce(context) {
|
|
78
|
+
const check = (node, name, kind) => {
|
|
79
|
+
if (name === null)
|
|
80
|
+
return;
|
|
81
|
+
const message = namingViolation(name, kind);
|
|
82
|
+
if (message !== null)
|
|
83
|
+
context.report({ node, message });
|
|
84
|
+
};
|
|
85
|
+
const checkBinding = (node, shorthand) => {
|
|
86
|
+
if (!node)
|
|
87
|
+
return;
|
|
88
|
+
switch (node.type) {
|
|
89
|
+
case "Identifier": {
|
|
90
|
+
if (shorthand || PLACEHOLDER_NAME.test(node.name))
|
|
91
|
+
return;
|
|
92
|
+
check(node, node.name, "variable");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
case "AssignmentPattern":
|
|
96
|
+
checkBinding(node.left, shorthand);
|
|
97
|
+
return;
|
|
98
|
+
case "RestElement":
|
|
99
|
+
checkBinding(node.argument, false);
|
|
100
|
+
return;
|
|
101
|
+
case "ArrayPattern": {
|
|
102
|
+
for (const element of node.elements)
|
|
103
|
+
checkBinding(element, false);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "ObjectPattern": {
|
|
107
|
+
for (const property of node.properties) {
|
|
108
|
+
if (property.type === "RestElement")
|
|
109
|
+
checkBinding(property.argument, false);
|
|
110
|
+
else
|
|
111
|
+
checkBinding(property.value, property.shorthand);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
default:
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
VariableDeclarator(node) {
|
|
121
|
+
checkBinding(node.id, false);
|
|
122
|
+
},
|
|
123
|
+
Property(node) {
|
|
124
|
+
if (node.computed || node.parent.type === "ObjectPattern")
|
|
125
|
+
return;
|
|
126
|
+
check(node.key, memberName(node.key), "objectLiteralMember");
|
|
127
|
+
},
|
|
128
|
+
TSPropertySignature(node) {
|
|
129
|
+
if (node.computed)
|
|
130
|
+
return;
|
|
131
|
+
check(node.key, memberName(node.key), "typeMember");
|
|
132
|
+
},
|
|
133
|
+
TSMethodSignature(node) {
|
|
134
|
+
if (node.computed)
|
|
135
|
+
return;
|
|
136
|
+
check(node.key, memberName(node.key), "typeMember");
|
|
137
|
+
},
|
|
138
|
+
TSEnumMember(node) {
|
|
139
|
+
check(node.id, memberName(node.id), "enumMember");
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { optionsOf } from "../../../lib/ast.js";
|
|
4
|
+
const DESIGN_SYSTEM_DIR = "src/components/ui";
|
|
5
|
+
const DESIGN_SYSTEM_ALIAS = "@/components/ui";
|
|
6
|
+
const PLATFORM_SUFFIX = /\.(?:ios|android|native|web)$/;
|
|
7
|
+
const LEGACY_EQUIVALENTS = {
|
|
8
|
+
Pressable: ["TouchableOpacity", "TouchableHighlight", "TouchableWithoutFeedback", "TouchableNativeFeedback"],
|
|
9
|
+
};
|
|
10
|
+
const NAME_OR_NAMES = { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] };
|
|
11
|
+
/** What one `use` entry may be. Declared here so oxlint rejects a typo'd key instead of silently dropping the ban. */
|
|
12
|
+
const USE_ENTRY_SCHEMA = {
|
|
13
|
+
anyOf: [
|
|
14
|
+
...NAME_OR_NAMES.anyOf,
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
replaces: NAME_OR_NAMES,
|
|
19
|
+
from: { type: "string" },
|
|
20
|
+
path: { type: "string" },
|
|
21
|
+
reason: { type: "string" },
|
|
22
|
+
},
|
|
23
|
+
required: ["replaces"],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
const toPascalCase = (name) => name
|
|
29
|
+
.split("-")
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
32
|
+
.join("");
|
|
33
|
+
const toKebabCase = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
34
|
+
const addBan = (banned, sourceModule, imported, replacement) => {
|
|
35
|
+
const byImportName = banned.get(sourceModule) ?? new Map();
|
|
36
|
+
byImportName.set(imported, replacement);
|
|
37
|
+
banned.set(sourceModule, byImportName);
|
|
38
|
+
};
|
|
39
|
+
const scanDesignSystem = (banned, dir, alias) => {
|
|
40
|
+
let files;
|
|
41
|
+
try {
|
|
42
|
+
files = readdirSync(isAbsolute(dir) ? dir : join(process.cwd(), dir));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
if (!file.endsWith(".tsx"))
|
|
49
|
+
continue;
|
|
50
|
+
const base = file.slice(0, -4).replace(PLATFORM_SUFFIX, "");
|
|
51
|
+
if (base === "index")
|
|
52
|
+
continue;
|
|
53
|
+
const name = toPascalCase(base);
|
|
54
|
+
const from = `${alias}/${base}`;
|
|
55
|
+
addBan(banned, "react-native", name, { name, from });
|
|
56
|
+
for (const legacy of LEGACY_EQUIVALENTS[name] ?? [])
|
|
57
|
+
addBan(banned, "react-native", legacy, { name, from });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const applyUse = (banned, alias, use) => {
|
|
61
|
+
for (const [component, entry] of Object.entries(use)) {
|
|
62
|
+
const config = typeof entry === "string" || Array.isArray(entry) ? { replaces: entry } : entry;
|
|
63
|
+
const replacement = {
|
|
64
|
+
name: component,
|
|
65
|
+
from: config.path ?? `${alias}/${toKebabCase(component)}`,
|
|
66
|
+
reason: config.reason,
|
|
67
|
+
};
|
|
68
|
+
for (const imported of [config.replaces].flat()) {
|
|
69
|
+
addBan(banned, config.from ?? "react-native", imported, replacement);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const designSystems = new Map();
|
|
74
|
+
const isExemptFile = (filename, exempt) => !!filename && exempt.some(fragment => filename.includes(fragment));
|
|
75
|
+
const designSystemFor = (options) => {
|
|
76
|
+
const dir = options.dir ?? DESIGN_SYSTEM_DIR;
|
|
77
|
+
const alias = options.alias ?? DESIGN_SYSTEM_ALIAS;
|
|
78
|
+
const exempt = options.exempt ?? [dir];
|
|
79
|
+
const cacheKey = JSON.stringify([dir, alias, options.use]);
|
|
80
|
+
let banned = designSystems.get(cacheKey);
|
|
81
|
+
if (banned === undefined) {
|
|
82
|
+
banned = new Map();
|
|
83
|
+
scanDesignSystem(banned, dir, alias);
|
|
84
|
+
if (options.use)
|
|
85
|
+
applyUse(banned, alias, options.use);
|
|
86
|
+
designSystems.set(cacheKey, banned);
|
|
87
|
+
}
|
|
88
|
+
return { banned, exempt };
|
|
89
|
+
};
|
|
90
|
+
export const useDesignSystem = {
|
|
91
|
+
meta: {
|
|
92
|
+
type: "problem",
|
|
93
|
+
docs: {
|
|
94
|
+
description: "Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped.",
|
|
95
|
+
},
|
|
96
|
+
schema: [
|
|
97
|
+
{
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
dir: { type: "string", minLength: 1 },
|
|
101
|
+
alias: { type: "string", minLength: 1 },
|
|
102
|
+
use: { type: "object", additionalProperties: USE_ENTRY_SCHEMA },
|
|
103
|
+
exempt: { type: "array", items: { type: "string" } },
|
|
104
|
+
},
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
defaultOff: true,
|
|
109
|
+
},
|
|
110
|
+
createOnce(context) {
|
|
111
|
+
let banned = new Map();
|
|
112
|
+
return {
|
|
113
|
+
before() {
|
|
114
|
+
const options = optionsOf(context, {});
|
|
115
|
+
const designSystem = designSystemFor(options);
|
|
116
|
+
banned = designSystem.banned;
|
|
117
|
+
if (banned.size === 0)
|
|
118
|
+
return false;
|
|
119
|
+
const { filename } = context;
|
|
120
|
+
if (isExemptFile(filename, designSystem.exempt))
|
|
121
|
+
return false;
|
|
122
|
+
return true;
|
|
123
|
+
},
|
|
124
|
+
ImportDeclaration(node) {
|
|
125
|
+
const source = node.source.value;
|
|
126
|
+
const bySource = banned.get(source);
|
|
127
|
+
if (bySource === undefined)
|
|
128
|
+
return;
|
|
129
|
+
for (const specifier of node.specifiers) {
|
|
130
|
+
if (specifier.type !== "ImportSpecifier")
|
|
131
|
+
continue;
|
|
132
|
+
const { imported } = specifier;
|
|
133
|
+
if (imported.type !== "Identifier")
|
|
134
|
+
continue;
|
|
135
|
+
const replacement = bySource.get(imported.name);
|
|
136
|
+
if (replacement === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
const reason = replacement.reason ? ` ${replacement.reason}` : "";
|
|
139
|
+
context.report({
|
|
140
|
+
node: specifier,
|
|
141
|
+
message: `Import \`${replacement.name}\` from "${replacement.from}" instead of \`${imported.name}\` from ${source}. The wrapper is where the theme tokens and app behavior live.${reason}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineModule } from "../../../lib/module.js";
|
|
2
|
+
import { preferEnum } from "./prefer-enum.js";
|
|
3
|
+
export default defineModule({
|
|
4
|
+
meta: { name: "@ashstack/zod" },
|
|
5
|
+
url: import.meta.url,
|
|
6
|
+
packages: ["zod"],
|
|
7
|
+
option: "zod",
|
|
8
|
+
docsWhen: "auto-enabled when `zod` is a dependency",
|
|
9
|
+
rules: {
|
|
10
|
+
"prefer-enum": preferEnum,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { problem } from "../../../lib/ast.js";
|
|
2
|
+
const MESSAGES = {
|
|
3
|
+
nativeEnum: "Call `z.enum()` here — it takes the same native enum object and params, and `z.nativeEnum()` is deprecated in Zod 4.",
|
|
4
|
+
literalUnion: "Replace this union with `z.enum([...])` listing the same string values, so invalid input reports one issue and the options stay reusable.",
|
|
5
|
+
};
|
|
6
|
+
const isZodCall = (node, method) => {
|
|
7
|
+
const { callee } = node;
|
|
8
|
+
if (callee.type !== "MemberExpression")
|
|
9
|
+
return false;
|
|
10
|
+
const { object, property } = callee;
|
|
11
|
+
if (object.type !== "Identifier" || object.name !== "z")
|
|
12
|
+
return false;
|
|
13
|
+
return property.type === "Identifier" && property.name === method;
|
|
14
|
+
};
|
|
15
|
+
const isStringLiteralCall = (node) => {
|
|
16
|
+
if (node?.type !== "CallExpression" || !isZodCall(node, "literal"))
|
|
17
|
+
return false;
|
|
18
|
+
const args = node.arguments;
|
|
19
|
+
if (args.length !== 1)
|
|
20
|
+
return false;
|
|
21
|
+
const [first] = args;
|
|
22
|
+
return first?.type === "Literal" && typeof first.value === "string";
|
|
23
|
+
};
|
|
24
|
+
export const preferEnum = problem("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.", {
|
|
25
|
+
createOnce(context) {
|
|
26
|
+
return {
|
|
27
|
+
CallExpression(node) {
|
|
28
|
+
if (isZodCall(node, "nativeEnum")) {
|
|
29
|
+
context.report({ node, message: MESSAGES.nativeEnum });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!isZodCall(node, "union"))
|
|
33
|
+
return;
|
|
34
|
+
const [members] = node.arguments;
|
|
35
|
+
if (members?.type !== "ArrayExpression")
|
|
36
|
+
return;
|
|
37
|
+
const { elements } = members;
|
|
38
|
+
if (elements.length === 0 || !elements.every(element => isStringLiteralCall(element)))
|
|
39
|
+
return;
|
|
40
|
+
context.report({ node, message: MESSAGES.literalUnion });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as core, coreModules } from "./core/index.js";
|
|
2
|
+
export { default as react, reactModules } from "./react/index.js";
|
|
3
|
+
export { default as reactNative, reactNativeModules, banGroups } from "./react-native/index.js";
|
|
4
|
+
export type { CoreOptions, ReactOptions, ReactNativeOptions, ModuleManifest } from "./lib/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AstNode, Rule, RuleBody, RuleContext, Visitor } from "./types.js";
|
|
2
|
+
type Predicate = (node: AstNode) => boolean;
|
|
3
|
+
export declare const findInSubtree: (node: unknown, predicate: Predicate) => AstNode | null;
|
|
4
|
+
export declare const subtreeHas: (node: unknown, predicate: Predicate) => boolean;
|
|
5
|
+
/** The called name: `foo()` and `a.b.foo()` both read as `foo`; anything else is `""`. */
|
|
6
|
+
export declare const calleeName: (node: AstNode | null | undefined) => string;
|
|
7
|
+
/** Every ancestor of `node`, innermost first. */
|
|
8
|
+
export declare const ancestors: (node: AstNode) => Generator<AstNode>;
|
|
9
|
+
export declare const enclosingCall: (node: AstNode, names: ReadonlySet<string>) => AstNode | null;
|
|
10
|
+
export declare const closestAncestor: (node: AstNode, types: ReadonlySet<string>) => AstNode | null;
|
|
11
|
+
export declare const hasAncestor: (node: AstNode, predicate: Predicate) => boolean;
|
|
12
|
+
export declare const isWithin: (node: AstNode, container: AstNode) => boolean;
|
|
13
|
+
export declare const crossesFunctionBefore: (node: AstNode, container: AstNode, functionTypes: ReadonlySet<string>) => boolean;
|
|
14
|
+
export declare const isMemberCall: (node: AstNode | null | undefined, method: string) => boolean;
|
|
15
|
+
/** The object a method call hangs off: `store.get()` reads as `store`. */
|
|
16
|
+
export declare const receiverName: (node: AstNode | null | undefined) => string | null;
|
|
17
|
+
/** The last segment of a JSX tag name: `Animated.View` reads as `View`. */
|
|
18
|
+
export declare const tagIdentifier: (node: AstNode | null | undefined) => string;
|
|
19
|
+
/** The full dotted JSX tag name: `Animated.View` reads as `Animated.View`. Empty when a segment is not a plain name. */
|
|
20
|
+
export declare const tagPath: (node: AstNode | null | undefined) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Which component a tag names, seeing through animation wrappers and nothing
|
|
23
|
+
* else: `Animated.View` and `AnimatedView` both read as `View`, while
|
|
24
|
+
* `Ui.View` reads as `""` — a different component that merely ends in `View`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const componentName: (node: AstNode | null | undefined) => string;
|
|
27
|
+
export declare const attributeName: (attribute: AstNode) => string;
|
|
28
|
+
export declare const importedSpecifiers: (node: AstNode, source: string) => AstNode[];
|
|
29
|
+
/** Function-ish node types, for ancestor walks. */
|
|
30
|
+
export declare const FUNCTION_TYPES: Set<string>;
|
|
31
|
+
/** Component or hook name: `Capitalized` or `useX`. */
|
|
32
|
+
export declare const COMPONENT_OR_HOOK: RegExp;
|
|
33
|
+
export declare const isFunction: (node: AstNode | null | undefined) => boolean;
|
|
34
|
+
/** Source-text gate for createOnce rules: skip whole files that cannot contain the pattern. */
|
|
35
|
+
export declare const gate: (context: RuleContext, ...markers: string[]) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* A rule's first option object, or `fallback` when the consumer passed none.
|
|
38
|
+
* oxlint types options as untyped JSON, so this is the one place a rule's
|
|
39
|
+
* option shape is asserted rather than checked.
|
|
40
|
+
*/
|
|
41
|
+
export declare const optionsOf: <T extends object>(context: RuleContext, fallback: T) => T;
|
|
42
|
+
/** Shorthand for a problem rule with a description. */
|
|
43
|
+
export declare const problem: (description: string, rule: RuleBody) => Rule;
|
|
44
|
+
export type { AstNode, Rule, Visitor };
|
package/dist/lib/ast.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const SKIPPED_KEYS = new Set(["parent", "loc", "range", "start", "end", "type"]);
|
|
2
|
+
const findInEach = (values, predicate) => {
|
|
3
|
+
for (const value of values) {
|
|
4
|
+
const found = findInSubtree(value, predicate);
|
|
5
|
+
if (found)
|
|
6
|
+
return found;
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
};
|
|
10
|
+
const childValues = (node) => {
|
|
11
|
+
const values = [];
|
|
12
|
+
for (const [key, value] of Object.entries(node)) {
|
|
13
|
+
if (SKIPPED_KEYS.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
if (value !== null && typeof value === "object")
|
|
16
|
+
values.push(value);
|
|
17
|
+
}
|
|
18
|
+
return values;
|
|
19
|
+
};
|
|
20
|
+
export const findInSubtree = (node, predicate) => {
|
|
21
|
+
if (node === null || typeof node !== "object")
|
|
22
|
+
return null;
|
|
23
|
+
if (Array.isArray(node))
|
|
24
|
+
return findInEach(node, predicate);
|
|
25
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
26
|
+
const candidate = node;
|
|
27
|
+
if (typeof candidate.type !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
if (predicate(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
return findInEach(childValues(candidate), predicate);
|
|
32
|
+
};
|
|
33
|
+
export const subtreeHas = (node, predicate) => findInSubtree(node, predicate) !== null;
|
|
34
|
+
/** The called name: `foo()` and `a.b.foo()` both read as `foo`; anything else is `""`. */
|
|
35
|
+
export const calleeName = (node) => {
|
|
36
|
+
if (node?.type !== "CallExpression")
|
|
37
|
+
return "";
|
|
38
|
+
const { callee } = node;
|
|
39
|
+
if (callee.type === "Identifier")
|
|
40
|
+
return callee.name;
|
|
41
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier")
|
|
42
|
+
return callee.property.name;
|
|
43
|
+
return "";
|
|
44
|
+
};
|
|
45
|
+
/** Every ancestor of `node`, innermost first. */
|
|
46
|
+
export const ancestors = function* (node) {
|
|
47
|
+
let current = node.parent;
|
|
48
|
+
while (current) {
|
|
49
|
+
yield current;
|
|
50
|
+
current = current.parent;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export const enclosingCall = (node, names) => {
|
|
54
|
+
for (const current of ancestors(node)) {
|
|
55
|
+
if (current.type === "CallExpression" && names.has(calleeName(current)))
|
|
56
|
+
return current;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
export const closestAncestor = (node, types) => {
|
|
61
|
+
for (const current of ancestors(node)) {
|
|
62
|
+
if (types.has(current.type))
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
export const hasAncestor = (node, predicate) => {
|
|
68
|
+
for (const current of ancestors(node)) {
|
|
69
|
+
if (predicate(current))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
export const isWithin = (node, container) => {
|
|
75
|
+
for (const current of ancestors(node)) {
|
|
76
|
+
if (current === container)
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
export const crossesFunctionBefore = (node, container, functionTypes) => {
|
|
82
|
+
for (const current of ancestors(node)) {
|
|
83
|
+
if (current === container)
|
|
84
|
+
return false;
|
|
85
|
+
if (functionTypes.has(current.type))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
export const isMemberCall = (node, method) => node?.type === "CallExpression" &&
|
|
91
|
+
node.callee.type === "MemberExpression" &&
|
|
92
|
+
!node.callee.computed &&
|
|
93
|
+
node.callee.property.type === "Identifier" &&
|
|
94
|
+
node.callee.property.name === method;
|
|
95
|
+
/** The object a method call hangs off: `store.get()` reads as `store`. */
|
|
96
|
+
export const receiverName = (node) => {
|
|
97
|
+
if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression")
|
|
98
|
+
return null;
|
|
99
|
+
const { object } = node.callee;
|
|
100
|
+
return object.type === "Identifier" ? object.name : null;
|
|
101
|
+
};
|
|
102
|
+
/** The last segment of a JSX tag name: `Animated.View` reads as `View`. */
|
|
103
|
+
export const tagIdentifier = (node) => {
|
|
104
|
+
let current = node;
|
|
105
|
+
while (current?.type === "JSXMemberExpression")
|
|
106
|
+
current = current.property;
|
|
107
|
+
if (current?.type === "JSXIdentifier" || current?.type === "Identifier")
|
|
108
|
+
return current.name;
|
|
109
|
+
return "";
|
|
110
|
+
};
|
|
111
|
+
/** The full dotted JSX tag name: `Animated.View` reads as `Animated.View`. Empty when a segment is not a plain name. */
|
|
112
|
+
export const tagPath = (node) => {
|
|
113
|
+
if (node?.type === "JSXIdentifier" || node?.type === "Identifier")
|
|
114
|
+
return node.name;
|
|
115
|
+
if (node?.type !== "JSXMemberExpression")
|
|
116
|
+
return "";
|
|
117
|
+
const object = tagPath(node.object);
|
|
118
|
+
const property = tagPath(node.property);
|
|
119
|
+
return object === "" || property === "" ? "" : `${object}.${property}`;
|
|
120
|
+
};
|
|
121
|
+
/** Namespaces and name prefixes that wrap a component without changing which component it is. */
|
|
122
|
+
const WRAPPER_PREFIX = /^(?:Animated|Reanimated)/;
|
|
123
|
+
/**
|
|
124
|
+
* Which component a tag names, seeing through animation wrappers and nothing
|
|
125
|
+
* else: `Animated.View` and `AnimatedView` both read as `View`, while
|
|
126
|
+
* `Ui.View` reads as `""` — a different component that merely ends in `View`.
|
|
127
|
+
*/
|
|
128
|
+
export const componentName = (node) => {
|
|
129
|
+
const path = tagPath(node);
|
|
130
|
+
const dot = path.lastIndexOf(".");
|
|
131
|
+
if (dot !== -1)
|
|
132
|
+
return WRAPPER_PREFIX.test(path.slice(0, dot)) ? path.slice(dot + 1) : "";
|
|
133
|
+
const unwrapped = path.replace(WRAPPER_PREFIX, "");
|
|
134
|
+
return unwrapped === "" ? path : unwrapped;
|
|
135
|
+
};
|
|
136
|
+
export const attributeName = (attribute) => {
|
|
137
|
+
if (attribute.type !== "JSXAttribute")
|
|
138
|
+
return "";
|
|
139
|
+
const { name } = attribute;
|
|
140
|
+
return name.type === "JSXIdentifier" ? name.name : name.name.name;
|
|
141
|
+
};
|
|
142
|
+
export const importedSpecifiers = (node, source) => node.type === "ImportDeclaration" && node.source.value === source ? [...node.specifiers] : [];
|
|
143
|
+
/** Function-ish node types, for ancestor walks. */
|
|
144
|
+
export const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
145
|
+
/** Component or hook name: `Capitalized` or `useX`. */
|
|
146
|
+
export const COMPONENT_OR_HOOK = /^(?:[A-Z]|use[A-Z])/;
|
|
147
|
+
export const isFunction = (node) => !!node && FUNCTION_TYPES.has(node.type);
|
|
148
|
+
/** Source-text gate for createOnce rules: skip whole files that cannot contain the pattern. */
|
|
149
|
+
export const gate = (context, ...markers) => markers.some(marker => context.sourceCode.text.includes(marker));
|
|
150
|
+
/**
|
|
151
|
+
* A rule's first option object, or `fallback` when the consumer passed none.
|
|
152
|
+
* oxlint types options as untyped JSON, so this is the one place a rule's
|
|
153
|
+
* option shape is asserted rather than checked.
|
|
154
|
+
*/
|
|
155
|
+
export const optionsOf = (context, fallback) =>
|
|
156
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
157
|
+
context.options[0] ?? fallback;
|
|
158
|
+
/** Shorthand for a problem rule with a description. */
|
|
159
|
+
export const problem = (description, rule) => ({
|
|
160
|
+
...rule,
|
|
161
|
+
meta: { type: "problem", ...rule.meta, docs: { description } },
|
|
162
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Should this rule group ship enabled? An explicit option wins; otherwise the
|
|
3
|
+
* group is on when one of `packages` is a dependency, and always on when
|
|
4
|
+
* `packages` is absent. Dependencies come from every package.json between the
|
|
5
|
+
* current working directory and the repo boundary — the directory holding
|
|
6
|
+
* `.git` — so in a monorepo the app package, the workspace root, or both
|
|
7
|
+
* count, and a stray package.json above the repo cannot enable anything.
|
|
8
|
+
*/
|
|
9
|
+
export declare const detect: (option: boolean | undefined, packages: string[] | undefined) => boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const dependencyNamesIn = (dir) => {
|
|
4
|
+
try {
|
|
5
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
6
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const isRepoBoundary = (dir) => existsSync(join(dir, ".git"));
|
|
13
|
+
const dependenciesByStartDir = new Map();
|
|
14
|
+
const dependenciesUpToRepoBoundary = () => {
|
|
15
|
+
const start = process.cwd();
|
|
16
|
+
const cached = dependenciesByStartDir.get(start);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const dependencies = new Set();
|
|
20
|
+
let dir = start;
|
|
21
|
+
for (;;) {
|
|
22
|
+
for (const name of dependencyNamesIn(dir))
|
|
23
|
+
dependencies.add(name);
|
|
24
|
+
if (isRepoBoundary(dir))
|
|
25
|
+
break;
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir)
|
|
28
|
+
break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
dependenciesByStartDir.set(start, dependencies);
|
|
32
|
+
return dependencies;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Should this rule group ship enabled? An explicit option wins; otherwise the
|
|
36
|
+
* group is on when one of `packages` is a dependency, and always on when
|
|
37
|
+
* `packages` is absent. Dependencies come from every package.json between the
|
|
38
|
+
* current working directory and the repo boundary — the directory holding
|
|
39
|
+
* `.git` — so in a monorepo the app package, the workspace root, or both
|
|
40
|
+
* count, and a stray package.json above the repo cannot enable anything.
|
|
41
|
+
*/
|
|
42
|
+
export const detect = (option, packages) => option ?? (packages === undefined || packages.some(name => dependenciesUpToRepoBoundary().has(name)));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OxlintConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Layer `delta` over `base` into one flat config — the way `react` is built
|
|
4
|
+
* from `core` plus a react delta. Plugin lists union, `overrides` append,
|
|
5
|
+
* `rules` and the other keyed blocks are last-wins per key. Entries compose
|
|
6
|
+
* with this rather than oxlint's `extends` so every entry is a single
|
|
7
|
+
* inspectable object and a consumer's own overrides stay trivial.
|
|
8
|
+
*/
|
|
9
|
+
export declare const mergeConfigs: (base: OxlintConfig, delta: OxlintConfig) => OxlintConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer `delta` over `base` into one flat config — the way `react` is built
|
|
3
|
+
* from `core` plus a react delta. Plugin lists union, `overrides` append,
|
|
4
|
+
* `rules` and the other keyed blocks are last-wins per key. Entries compose
|
|
5
|
+
* with this rather than oxlint's `extends` so every entry is a single
|
|
6
|
+
* inspectable object and a consumer's own overrides stay trivial.
|
|
7
|
+
*/
|
|
8
|
+
export const mergeConfigs = (base, delta) => ({
|
|
9
|
+
...base,
|
|
10
|
+
...delta,
|
|
11
|
+
plugins: [...new Set([...(base.plugins ?? []), ...(delta.plugins ?? [])])],
|
|
12
|
+
jsPlugins: [...new Set([...(base.jsPlugins ?? []), ...(delta.jsPlugins ?? [])])],
|
|
13
|
+
env: { ...base.env, ...delta.env },
|
|
14
|
+
globals: { ...base.globals, ...delta.globals },
|
|
15
|
+
categories: { ...base.categories, ...delta.categories },
|
|
16
|
+
rules: { ...base.rules, ...delta.rules },
|
|
17
|
+
overrides: [...(base.overrides ?? []), ...(delta.overrides ?? [])],
|
|
18
|
+
});
|