@ashstack/lint 0.1.0 → 0.2.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.
Files changed (102) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -222
  3. package/dist/core/rules/core/components-tsx-only.js +1 -37
  4. package/dist/core/rules/core/hoist-intl.js +1 -32
  5. package/dist/core/rules/core/index.js +1 -18
  6. package/dist/core/rules/core/no-comments.js +1 -194
  7. package/dist/core/rules/core/no-naming-convention.js +1 -143
  8. package/dist/core/rules/core/use-design-system.js +1 -147
  9. package/dist/core/rules/zod/index.js +1 -12
  10. package/dist/core/rules/zod/prefer-enum.js +1 -44
  11. package/dist/index.js +1 -3
  12. package/dist/lib/ast.js +1 -162
  13. package/dist/lib/detect.js +1 -42
  14. package/dist/lib/merge.js +1 -18
  15. package/dist/lib/module.js +1 -49
  16. package/dist/lib/types.js +1 -1
  17. package/dist/react/index.js +1 -126
  18. package/dist/react/rules/i18n/index.js +1 -16
  19. package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
  20. package/dist/react/rules/i18n/no-bare-text.js +1 -26
  21. package/dist/react/rules/i18n/no-bare-toast.js +1 -38
  22. package/dist/react/rules/query/index.js +1 -20
  23. package/dist/react/rules/query/next-page-param-undefined.js +1 -26
  24. package/dist/react/rules/query/no-deprecated-filters.js +1 -63
  25. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
  26. package/dist/react/rules/query/no-inline-keys.js +1 -107
  27. package/dist/react/rules/query/require-destructured-hooks.js +1 -41
  28. package/dist/react/rules/query/shared.js +1 -10
  29. package/dist/react/rules/zustand/index.js +1 -12
  30. package/dist/react/rules/zustand/require-selector.js +1 -62
  31. package/dist/react-native/index.js +1 -113
  32. package/dist/react-native/rules/keyboard/index.js +1 -18
  33. package/dist/react-native/rules/legend-list/index.js +1 -51
  34. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
  35. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
  36. package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
  37. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
  38. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
  39. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
  40. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
  41. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
  42. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
  43. package/dist/react-native/rules/legend-list/required-props.js +1 -23
  44. package/dist/react-native/rules/legend-list/shared.js +1 -25
  45. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
  46. package/dist/react-native/rules/legend-state/index.js +1 -35
  47. package/dist/react-native/rules/legend-state/naming.js +1 -26
  48. package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
  49. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
  50. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
  51. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
  52. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
  53. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
  54. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
  55. package/dist/react-native/rules/legend-state/shared.js +1 -23
  56. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
  57. package/dist/react-native/rules/react-native/index.js +1 -50
  58. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
  59. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
  60. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
  61. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
  62. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
  63. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
  64. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
  65. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
  66. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
  67. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
  68. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
  69. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
  70. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
  71. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
  72. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
  73. package/dist/react-native/rules/reanimated/index.js +1 -51
  74. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
  75. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
  76. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
  77. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
  78. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
  79. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
  80. package/dist/react-native/rules/reanimated/shared.js +1 -13
  81. package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
  82. package/dist/react-native/rules/skia/index.js +1 -21
  83. package/dist/react-native/rules/turbo-image/index.js +1 -14
  84. package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
  85. package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
  86. package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
  87. package/dist/react-native/rules/unistyles/content-container.js +1 -73
  88. package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
  89. package/dist/react-native/rules/unistyles/index.js +1 -58
  90. package/dist/react-native/rules/unistyles/insets.js +1 -92
  91. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
  92. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
  93. package/dist/react-native/rules/unistyles/no-margin.js +1 -19
  94. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
  95. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
  96. package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
  97. package/dist/react-native/rules/unistyles/shared.js +1 -70
  98. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
  99. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
  100. package/dist/react-native/stylesheet.js +1 -44
  101. package/package.json +3 -4
  102. package/RULES.md +0 -2881
@@ -1,143 +1 @@
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
- });
1
+ import{problem as A}from"../../../lib/ast.js";const f=/^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u,p=/^[a-z][A-Za-z0-9]*$/,E=/^[A-Z][A-Za-z0-9]*$/,_=/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/,b=/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/,S=/^_+$/,C={camelCase:(t)=>p.test(t),PascalCase:(t)=>E.test(t)&&/[a-z]/.test(t),CONSTANT_CASE:(t)=>_.test(t),snake_case:(t)=>b.test(t)},m=["camelCase","snake_case","CONSTANT_CASE","PascalCase"],N=String.raw`\$?`,o=String.raw`[$_]*([A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)_*${N}`,g=String.raw`(?:[0-9.]+)`,T=["enableFullScreenImage_legacy","experimental_backgroundImage","unstable_conditionNames"],R=["unstable_settings"],i=(...t)=>new RegExp(`^(?:${t.join("|")})$`),y={objectLiteralMember:{match:i(...T,g,o),formats:m,label:"object property"},typeMember:{match:i(o),formats:m,label:"type member"},variable:{match:i(...R,o),formats:["camelCase","CONSTANT_CASE","PascalCase"],label:"variable"},enumMember:{match:null,formats:["CONSTANT_CASE","PascalCase"],label:"enum member"}},u=(t,n)=>C[t]?.(n)===!0,c=(t)=>{if(!t)return null;if(t.type==="Identifier")return t.name;if(t.type!=="Literal")return null;if(typeof t.value==="string")return f.test(t.value)?t.value:null;if(typeof t.value==="number")return t.raw??String(t.value);return null},M=(t,n)=>{const r=y[n],e=`Rename this ${r.label} \`${t}\` to ${r.formats.join(", ")}.`;if(r.match===null)return r.formats.some((l)=>u(l,t))?null:e;const s=r.match.exec(t);if(!s)return`Rename this ${r.label} \`${t}\` to a plain ${r.formats.join("/")} identifier, with at most a leading \`_\` or a trailing \`$\`.`;const a=s[1];if(!a)return null;return r.formats.some((l)=>u(l,a))?null:e};export const noNamingConvention=A("Require a variable, object property, type member or enum member to use one of the casings allowed for its kind.",{createOnce(t){const n=(e,s,a)=>{if(s===null)return;const l=M(s,a);if(l!==null)t.report({node:e,message:l})},r=(e,s)=>{if(!e)return;switch(e.type){case"Identifier":{if(s||S.test(e.name))return;n(e,e.name,"variable");return}case"AssignmentPattern":r(e.left,s);return;case"RestElement":r(e.argument,!1);return;case"ArrayPattern":{for(const a of e.elements)r(a,!1);return}case"ObjectPattern":{for(const a of e.properties)if(a.type==="RestElement")r(a.argument,!1);else r(a.value,a.shorthand);return}default:return}};return{VariableDeclarator(e){r(e.id,!1)},Property(e){if(e.computed||e.parent.type==="ObjectPattern")return;n(e.key,c(e.key),"objectLiteralMember")},TSPropertySignature(e){if(e.computed)return;n(e.key,c(e.key),"typeMember")},TSMethodSignature(e){if(e.computed)return;n(e.key,c(e.key),"typeMember")},TSEnumMember(e){n(e.id,c(e.id),"enumMember")}}}});
@@ -1,147 +1 @@
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
- };
1
+ import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",h="@/components/ui",b=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),w=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},I=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(b,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},O=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${w(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,T=(e,t)=>!!e&&t.some((r)=>e.includes(r)),_=(e)=>{const t=e.dir??g,r=e.alias??h,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;I(s,t,r);if(e.use)O(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{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."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=_(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(T(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}. The wrapper is where the theme tokens and app behavior live.${c}`})}}}}};
@@ -1,12 +1 @@
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
- });
1
+ import{defineModule as e}from"../../../lib/module.js";import{preferEnum as o}from"./prefer-enum.js";export default e({meta:{name:"@ashstack/zod"},url:import.meta.url,packages:["zod"],option:"zod",docsWhen:"auto-enabled when `zod` is a dependency",rules:{"prefer-enum":o}});
@@ -1,44 +1 @@
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
- });
1
+ import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — it takes the same native enum object and params, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values, so invalid input reports one issue and the options stay reusable."},i=(t,e)=>{const{callee:n}=t;if(n.type!=="MemberExpression")return!1;const{object:r,property:s}=n;if(r.type!=="Identifier"||r.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},o=(t)=>{if(t?.type!=="CallExpression"||!i(t,"literal"))return!1;const e=t.arguments;if(e.length!==1)return!1;const[n]=e;return n?.type==="Literal"&&typeof n.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(t){return{CallExpression(e){if(i(e,"nativeEnum")){t.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[n]=e.arguments;if(n?.type!=="ArrayExpression")return;const{elements:r}=n;if(r.length===0||!r.every((s)=>o(s)))return;t.report({node:e,message:a.literalUnion})}}}});
package/dist/index.js CHANGED
@@ -1,3 +1 @@
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";
1
+ export{default as core,coreModules}from"./core/index.js";export{default as react,reactModules}from"./react/index.js";export{default as reactNative,reactNativeModules,banGroups}from"./react-native/index.js";
package/dist/lib/ast.js CHANGED
@@ -1,162 +1 @@
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
- });
1
+ const c=new Set(["parent","loc","range","start","end","type"]),o=(t,e)=>{for(const r of t){const n=findInSubtree(r,e);if(n)return n}return null},p=(t)=>{const e=[];for(const[r,n]of Object.entries(t)){if(c.has(r))continue;if(n!==null&&typeof n==="object")e.push(n)}return e};export const findInSubtree=(t,e)=>{if(t===null||typeof t!=="object")return null;if(Array.isArray(t))return o(t,e);const r=t;if(typeof r.type!=="string")return null;if(e(r))return r;return o(p(r),e)},subtreeHas=(t,e)=>findInSubtree(t,e)!==null,calleeName=(t)=>{if(t?.type!=="CallExpression")return"";const{callee:e}=t;if(e.type==="Identifier")return e.name;if(e.type==="MemberExpression"&&e.property.type==="Identifier")return e.property.name;return""},ancestors=function*(t){let e=t.parent;while(e){yield e;e=e.parent}},enclosingCall=(t,e)=>{for(const r of ancestors(t))if(r.type==="CallExpression"&&e.has(calleeName(r)))return r;return null},closestAncestor=(t,e)=>{for(const r of ancestors(t))if(e.has(r.type))return r;return null},hasAncestor=(t,e)=>{for(const r of ancestors(t))if(e(r))return!0;return!1},isWithin=(t,e)=>{for(const r of ancestors(t))if(r===e)return!0;return!1},crossesFunctionBefore=(t,e,r)=>{for(const n of ancestors(t)){if(n===e)return!1;if(r.has(n.type))return!0}return!1},isMemberCall=(t,e)=>t?.type==="CallExpression"&&t.callee.type==="MemberExpression"&&!t.callee.computed&&t.callee.property.type==="Identifier"&&t.callee.property.name===e,receiverName=(t)=>{if(t?.type!=="CallExpression"||t.callee.type!=="MemberExpression")return null;const{object:e}=t.callee;return e.type==="Identifier"?e.name:null},tagIdentifier=(t)=>{let e=t;while(e?.type==="JSXMemberExpression")e=e.property;if(e?.type==="JSXIdentifier"||e?.type==="Identifier")return e.name;return""},tagPath=(t)=>{if(t?.type==="JSXIdentifier"||t?.type==="Identifier")return t.name;if(t?.type!=="JSXMemberExpression")return"";const e=tagPath(t.object),r=tagPath(t.property);return e===""||r===""?"":`${e}.${r}`};const s=/^(?:Animated|Reanimated)/;export const componentName=(t)=>{const e=tagPath(t),r=e.lastIndexOf(".");if(r!==-1)return s.test(e.slice(0,r))?e.slice(r+1):"";const n=e.replace(s,"");return n===""?e:n},attributeName=(t)=>{if(t.type!=="JSXAttribute")return"";const{name:e}=t;return e.type==="JSXIdentifier"?e.name:e.name.name},importedSpecifiers=(t,e)=>t.type==="ImportDeclaration"&&t.source.value===e?[...t.specifiers]:[],FUNCTION_TYPES=new Set(["FunctionDeclaration","FunctionExpression","ArrowFunctionExpression"]),COMPONENT_OR_HOOK=/^(?:[A-Z]|use[A-Z])/,isFunction=(t)=>!!t&&FUNCTION_TYPES.has(t.type),gate=(t,...e)=>e.some((r)=>t.sourceCode.text.includes(r)),optionsOf=(t,e)=>t.options[0]??e,problem=(t,e)=>({...e,meta:{type:"problem",...e.meta,docs:{description:t}}});
@@ -1,42 +1 @@
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)));
1
+ import{existsSync as i,readFileSync as p}from"node:fs";import{dirname as a,join as c}from"node:path";const f=(e)=>{try{const n=JSON.parse(p(c(e,"package.json"),"utf8"));return Object.keys({...n.dependencies,...n.devDependencies,...n.peerDependencies})}catch{return[]}},m=(e)=>i(c(e,".git")),o=new Map,u=()=>{const e=process.cwd(),n=o.get(e);if(n)return n;const t=new Set;let r=e;for(;;){for(const d of f(r))t.add(d);if(m(r))break;const s=a(r);if(s===r)break;r=s}o.set(e,t);return t};export const detect=(e,n)=>e??(n===void 0||n.some((t)=>u().has(t)));
package/dist/lib/merge.js CHANGED
@@ -1,18 +1 @@
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
- });
1
+ export const mergeConfigs=(s,g)=>({...s,...g,plugins:[...new Set([...s.plugins??[],...g.plugins??[]])],jsPlugins:[...new Set([...s.jsPlugins??[],...g.jsPlugins??[]])],env:{...s.env,...g.env},globals:{...s.globals,...g.globals},categories:{...s.categories,...g.categories},rules:{...s.rules,...g.rules},overrides:[...s.overrides??[],...g.overrides??[]]});
@@ -1,49 +1 @@
1
- import { fileURLToPath } from "node:url";
2
- import { detect } from "./detect.js";
3
- /**
4
- * Identity helper: types the manifest and keeps the shape honest. The returned
5
- * object is handed to oxlint as the plugin (it ignores the manifest extras).
6
- */
7
- export const defineModule = (manifest) => manifest;
8
- const shortName = (module) => module.meta.name.slice("@ashstack/".length);
9
- const isModuleEnabled = (module, options) => detect(module.option === undefined ? undefined : options[module.option], module.packages);
10
- const enabledRuleIds = (module) => {
11
- const ids = [];
12
- for (const [name, rule] of Object.entries(module.rules)) {
13
- if (rule.meta.defaultOff)
14
- continue;
15
- if (rule.meta.packages && !detect(undefined, rule.meta.packages))
16
- continue;
17
- ids.push(`${module.meta.name}/${name}`);
18
- }
19
- return ids;
20
- };
21
- const addRestrictedImports = (into, from) => {
22
- into.paths.push(...(from?.paths ?? []));
23
- into.patterns.push(...(from?.patterns ?? []));
24
- };
25
- /**
26
- * Turn manifests + options into the config fragments an entry spreads in:
27
- * which plugin files to load, which rules to enable, and the merged import
28
- * bans from the enabled modules plus the enabled ban-only groups.
29
- */
30
- export const composeModules = (modules, options, banGroups = []) => {
31
- const composed = { jsPlugins: [], rules: {}, restricted: { paths: [], patterns: [] } };
32
- for (const module of modules) {
33
- if (!isModuleEnabled(module, options))
34
- continue;
35
- const ids = enabledRuleIds(module);
36
- if (ids.length > 0)
37
- composed.jsPlugins.push(fileURLToPath(module.url));
38
- for (const id of ids)
39
- composed.rules[id] = "error";
40
- addRestrictedImports(composed.restricted, module.restrictedImports);
41
- }
42
- for (const group of banGroups) {
43
- if (!detect(undefined, group.packages))
44
- continue;
45
- addRestrictedImports(composed.restricted, group.restrictedImports);
46
- }
47
- return composed;
48
- };
49
- export { shortName };
1
+ import{fileURLToPath as d}from"node:url";import{detect as r}from"./detect.js";export const defineModule=(e)=>e;const shortName=(e)=>e.meta.name.slice(10),p=(e,t)=>r(e.option===void 0?void 0:t[e.option],e.packages),u=(e)=>{const t=[];for(const[o,s]of Object.entries(e.rules)){if(s.meta.defaultOff)continue;if(s.meta.packages&&!r(void 0,s.meta.packages))continue;t.push(`${e.meta.name}/${o}`)}return t},i=(e,t)=>{e.paths.push(...t?.paths??[]);e.patterns.push(...t?.patterns??[])};export const composeModules=(e,t,o=[])=>{const s={jsPlugins:[],rules:{},restricted:{paths:[],patterns:[]}};for(const n of e){if(!p(n,t))continue;const c=u(n);if(c.length>0)s.jsPlugins.push(d(n.url));for(const a of c)s.rules[a]="error";i(s.restricted,n.restrictedImports)}for(const n of o){if(!r(void 0,n.packages))continue;i(s.restricted,n.restrictedImports)}return s};export{shortName};
package/dist/lib/types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export{};