@rdjperron/lint 0.1.0 → 0.3.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/README.md +147 -57
- package/dist/config.d.mts +122 -0
- package/dist/config.mjs +218 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/{anti-slop.mjs → index.mjs} +290 -97
- package/dist/index.mjs.map +1 -0
- package/dist/verify.d.mts +92 -0
- package/dist/verify.mjs +400 -0
- package/dist/verify.mjs.map +1 -0
- package/fixtures/README.md +22 -23
- package/fixtures/{anti-slop/no-array-filter-map.fixture.ts → no-array-filter-map.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-chained-type-assertions.fixture.ts → no-chained-type-assertions.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-conditional-empty-object-spread.fixture.ts → no-conditional-empty-object-spread.fixture.ts} +1 -1
- package/fixtures/{i18n/no-hardcoded-jsx-text.fixture.tsx → no-hardcoded-jsx-text.fixture.tsx} +1 -1
- package/fixtures/{anti-slop/no-known-value-widening.fixture.ts → no-known-value-widening.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-module-mocking.fixture.ts → no-module-mocking.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-object-parameters.fixture.ts → no-object-parameters.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reduce-accumulator-copy.fixture.ts → no-reduce-accumulator-copy.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reflect-apply.fixture.ts → no-reflect-apply.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reflect-get.fixture.ts → no-reflect-get.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-runtime-typeof.fixture.ts → no-runtime-typeof.fixture.ts} +1 -1
- package/fixtures/no-shape-in-symbol-names.fixture.ts +2 -0
- package/fixtures/{anti-slop/no-unknown-parameters.fixture.ts → no-unknown-parameters.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-unknown-returns.fixture.ts → no-unknown-returns.fixture.ts} +1 -1
- package/fixtures/no-unknown-type-aliases.fixture.ts +2 -0
- package/fixtures/no-unsafe-dictionary-type.fixture.ts +2 -0
- package/fixtures/{react/no-use-effect.fixture.tsx → no-use-effect.fixture.tsx} +1 -1
- package/fixtures/{anti-slop/no-widen-then-assert.fixture.ts → no-widen-then-assert.fixture.ts} +1 -1
- package/fixtures/{anti-slop/require-safety-comment-for-type-assertion.fixture.ts → require-safety-comment-for-type-assertion.fixture.ts} +1 -1
- package/fixtures/{expo/static-public-env.fixture.ts → static-public-env.fixture.ts} +1 -1
- package/package.json +26 -26
- package/dist/anti-slop.d.mts +0 -6
- package/dist/anti-slop.mjs.map +0 -1
- package/dist/expo.d.mts +0 -5
- package/dist/expo.mjs +0 -51
- package/dist/expo.mjs.map +0 -1
- package/dist/i18n.d.mts +0 -5
- package/dist/i18n.mjs +0 -50
- package/dist/i18n.mjs.map +0 -1
- package/dist/react.d.mts +0 -5
- package/dist/react.mjs +0 -73
- package/dist/react.mjs.map +0 -1
- package/dist/rule-options-DrAXz8BD.mjs +0 -18
- package/dist/rule-options-DrAXz8BD.mjs.map +0 -1
- package/fixtures/anti-slop/no-shape-in-symbol-names.fixture.ts +0 -2
- package/fixtures/anti-slop/no-unknown-type-aliases.fixture.ts +0 -2
- package/fixtures/anti-slop/no-unsafe-dictionary-type.fixture.ts +0 -2
|
@@ -97,89 +97,6 @@ const noArrayFilterMapRule = defineRule({
|
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
99
|
//#endregion
|
|
100
|
-
//#region src/anti-slop/rules/no-reduce-accumulator-copy.ts
|
|
101
|
-
function enclosingReducer(node) {
|
|
102
|
-
let parent = node.parent;
|
|
103
|
-
while (parent !== null) {
|
|
104
|
-
if (parent.type === "FunctionDeclaration") return null;
|
|
105
|
-
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
106
|
-
const callback = parent;
|
|
107
|
-
let owner = callback.parent;
|
|
108
|
-
while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
|
|
109
|
-
if (owner?.type !== "CallExpression") return null;
|
|
110
|
-
const method = arrayMethodTarget(owner.callee);
|
|
111
|
-
const firstArgument = owner.arguments[0];
|
|
112
|
-
if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === void 0 || unwrapArrayExpression(firstArgument) !== callback) return null;
|
|
113
|
-
const firstParameter = callback.params[0];
|
|
114
|
-
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
115
|
-
if (accumulator?.type !== "Identifier") return null;
|
|
116
|
-
return {
|
|
117
|
-
callback,
|
|
118
|
-
accumulator,
|
|
119
|
-
initialValue: owner.arguments[1]
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
parent = parent.parent;
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
function referencesAccumulator(sourceCode, node, accumulator, visited = /* @__PURE__ */ new Set()) {
|
|
127
|
-
const variable = resolveArrayBinding(sourceCode, node);
|
|
128
|
-
if (variable === null || visited.has(variable)) return false;
|
|
129
|
-
if (variable === accumulator) return true;
|
|
130
|
-
visited.add(variable);
|
|
131
|
-
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
132
|
-
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
function isGlobalCopyOwner(sourceCode, node, name) {
|
|
136
|
-
node = unwrapArrayExpression(node);
|
|
137
|
-
if (node.type !== "Identifier" || node.name !== name) return false;
|
|
138
|
-
const variable = resolveArrayBinding(sourceCode, node);
|
|
139
|
-
return variable === null || variable.defs.length === 0;
|
|
140
|
-
}
|
|
141
|
-
/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
|
|
142
|
-
const noReduceAccumulatorCopyRule = defineRule({
|
|
143
|
-
meta: {
|
|
144
|
-
type: "problem",
|
|
145
|
-
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
146
|
-
messages: { accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap." }
|
|
147
|
-
},
|
|
148
|
-
createOnce(context) {
|
|
149
|
-
return { CallExpression(node) {
|
|
150
|
-
const method = arrayMethodTarget(node.callee);
|
|
151
|
-
if (method === null) return;
|
|
152
|
-
const reducer = enclosingReducer(node);
|
|
153
|
-
if (reducer === null) return;
|
|
154
|
-
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
|
|
155
|
-
if (accumulator === void 0) return;
|
|
156
|
-
const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
157
|
-
let copiesAccumulator = false;
|
|
158
|
-
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
159
|
-
const target = node.arguments[0];
|
|
160
|
-
copiesAccumulator = target !== void 0 && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
|
|
161
|
-
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
162
|
-
const source = node.arguments[0];
|
|
163
|
-
copiesAccumulator = source !== void 0 && isAccumulator(source);
|
|
164
|
-
} else if ([
|
|
165
|
-
"concat",
|
|
166
|
-
"slice",
|
|
167
|
-
"toSpliced",
|
|
168
|
-
"toSorted",
|
|
169
|
-
"toReversed",
|
|
170
|
-
"with"
|
|
171
|
-
].includes(method.name)) {
|
|
172
|
-
const initialValue = reducer.initialValue;
|
|
173
|
-
copiesAccumulator = initialValue !== void 0 && isKnownArrayExpression(context.sourceCode, initialValue) && isAccumulator(method.object);
|
|
174
|
-
}
|
|
175
|
-
if (copiesAccumulator) context.report({
|
|
176
|
-
node,
|
|
177
|
-
messageId: "accumulatorCopy"
|
|
178
|
-
});
|
|
179
|
-
} };
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
//#endregion
|
|
183
100
|
//#region src/anti-slop/rules/no-chained-type-assertions.ts
|
|
184
101
|
function isTypeAssertionExpression(node) {
|
|
185
102
|
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
@@ -934,7 +851,7 @@ const moduleMockMethods = /* @__PURE__ */ new Set([
|
|
|
934
851
|
"mock",
|
|
935
852
|
"unstable_mockModule"
|
|
936
853
|
]);
|
|
937
|
-
function importedName(node) {
|
|
854
|
+
function importedName$1(node) {
|
|
938
855
|
if (node.type !== "ImportSpecifier") return null;
|
|
939
856
|
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
940
857
|
}
|
|
@@ -946,7 +863,7 @@ function isTestFrameworkObject(sourceCode, expression) {
|
|
|
946
863
|
return variable.defs.some((definition) => {
|
|
947
864
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
|
|
948
865
|
const source = definition.parent.source.value;
|
|
949
|
-
const name = importedName(definition.node);
|
|
866
|
+
const name = importedName$1(definition.node);
|
|
950
867
|
return source === "vitest" && name === "vi" || source === "@jest/globals" && name === "jest";
|
|
951
868
|
});
|
|
952
869
|
}
|
|
@@ -1020,6 +937,89 @@ const noObjectParametersRule = defineRule({
|
|
|
1020
937
|
}
|
|
1021
938
|
});
|
|
1022
939
|
//#endregion
|
|
940
|
+
//#region src/anti-slop/rules/no-reduce-accumulator-copy.ts
|
|
941
|
+
function enclosingReducer(node) {
|
|
942
|
+
let parent = node.parent;
|
|
943
|
+
while (parent !== null) {
|
|
944
|
+
if (parent.type === "FunctionDeclaration") return null;
|
|
945
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
946
|
+
const callback = parent;
|
|
947
|
+
let owner = callback.parent;
|
|
948
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
|
|
949
|
+
if (owner?.type !== "CallExpression") return null;
|
|
950
|
+
const method = arrayMethodTarget(owner.callee);
|
|
951
|
+
const firstArgument = owner.arguments[0];
|
|
952
|
+
if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === void 0 || unwrapArrayExpression(firstArgument) !== callback) return null;
|
|
953
|
+
const firstParameter = callback.params[0];
|
|
954
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
955
|
+
if (accumulator?.type !== "Identifier") return null;
|
|
956
|
+
return {
|
|
957
|
+
callback,
|
|
958
|
+
accumulator,
|
|
959
|
+
initialValue: owner.arguments[1]
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
parent = parent.parent;
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
function referencesAccumulator(sourceCode, node, accumulator, visited = /* @__PURE__ */ new Set()) {
|
|
967
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
968
|
+
if (variable === null || visited.has(variable)) return false;
|
|
969
|
+
if (variable === accumulator) return true;
|
|
970
|
+
visited.add(variable);
|
|
971
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
972
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
function isGlobalCopyOwner(sourceCode, node, name) {
|
|
976
|
+
node = unwrapArrayExpression(node);
|
|
977
|
+
if (node.type !== "Identifier" || node.name !== name) return false;
|
|
978
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
979
|
+
return variable === null || variable.defs.length === 0;
|
|
980
|
+
}
|
|
981
|
+
/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
|
|
982
|
+
const noReduceAccumulatorCopyRule = defineRule({
|
|
983
|
+
meta: {
|
|
984
|
+
type: "problem",
|
|
985
|
+
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
986
|
+
messages: { accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap." }
|
|
987
|
+
},
|
|
988
|
+
createOnce(context) {
|
|
989
|
+
return { CallExpression(node) {
|
|
990
|
+
const method = arrayMethodTarget(node.callee);
|
|
991
|
+
if (method === null) return;
|
|
992
|
+
const reducer = enclosingReducer(node);
|
|
993
|
+
if (reducer === null) return;
|
|
994
|
+
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
|
|
995
|
+
if (accumulator === void 0) return;
|
|
996
|
+
const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
997
|
+
let copiesAccumulator = false;
|
|
998
|
+
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
999
|
+
const target = node.arguments[0];
|
|
1000
|
+
copiesAccumulator = target !== void 0 && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
|
|
1001
|
+
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
1002
|
+
const source = node.arguments[0];
|
|
1003
|
+
copiesAccumulator = source !== void 0 && isAccumulator(source);
|
|
1004
|
+
} else if ([
|
|
1005
|
+
"concat",
|
|
1006
|
+
"slice",
|
|
1007
|
+
"toSpliced",
|
|
1008
|
+
"toSorted",
|
|
1009
|
+
"toReversed",
|
|
1010
|
+
"with"
|
|
1011
|
+
].includes(method.name)) {
|
|
1012
|
+
const initialValue = reducer.initialValue;
|
|
1013
|
+
copiesAccumulator = initialValue !== void 0 && isKnownArrayExpression(context.sourceCode, initialValue) && isAccumulator(method.object);
|
|
1014
|
+
}
|
|
1015
|
+
if (copiesAccumulator) context.report({
|
|
1016
|
+
node,
|
|
1017
|
+
messageId: "accumulatorCopy"
|
|
1018
|
+
});
|
|
1019
|
+
} };
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
//#endregion
|
|
1023
1023
|
//#region src/anti-slop/shared/reflect-method.ts
|
|
1024
1024
|
function isGlobalReflect(sourceCode, expression) {
|
|
1025
1025
|
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
@@ -1125,8 +1125,9 @@ const noRuntimeTypeofRule = defineRule({
|
|
|
1125
1125
|
//#endregion
|
|
1126
1126
|
//#region src/anti-slop/rules/no-shape-in-symbol-names.ts
|
|
1127
1127
|
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
1128
|
+
const IDENTIFIER_WORD = /[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/gu;
|
|
1128
1129
|
function containsForbiddenSymbolName(name) {
|
|
1129
|
-
return name.toLowerCase().
|
|
1130
|
+
return (name.match(IDENTIFIER_WORD) ?? []).some((word) => word.toLowerCase().startsWith(FORBIDDEN_SYMBOL_NAME));
|
|
1130
1131
|
}
|
|
1131
1132
|
/** Return whether an identifier names a statically accessed member owned by another value. */
|
|
1132
1133
|
function isBorrowedMemberName(node) {
|
|
@@ -1134,11 +1135,11 @@ function isBorrowedMemberName(node) {
|
|
|
1134
1135
|
if (parent === null || parent.type !== "MemberExpression") return false;
|
|
1135
1136
|
return parent.property === node && parent.computed === false;
|
|
1136
1137
|
}
|
|
1137
|
-
/** Ban the
|
|
1138
|
+
/** Ban the word "shape" in every JavaScript and TypeScript symbol name. */
|
|
1138
1139
|
const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
1139
1140
|
meta: {
|
|
1140
1141
|
type: "problem",
|
|
1141
|
-
docs: { description: "Disallow the
|
|
1142
|
+
docs: { description: "Disallow the word \"shape\" in JavaScript, TypeScript, private, and JSX symbol names." },
|
|
1142
1143
|
messages: { forbiddenSymbolName: "Rename symbol \"{{name}}\" for its domain role; \"shape\" describes structure rather than ownership." }
|
|
1143
1144
|
},
|
|
1144
1145
|
createOnce(context) {
|
|
@@ -1664,31 +1665,223 @@ const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
|
1664
1665
|
}
|
|
1665
1666
|
});
|
|
1666
1667
|
//#endregion
|
|
1667
|
-
//#region src/
|
|
1668
|
-
/**
|
|
1669
|
-
|
|
1670
|
-
|
|
1668
|
+
//#region src/shared/rule-options.ts
|
|
1669
|
+
/**
|
|
1670
|
+
* Rule options arrive as parsed JSON from the linter config, so this is the one
|
|
1671
|
+
* place their shape is checked. Returns the first option when it is an object.
|
|
1672
|
+
*/
|
|
1673
|
+
function firstOptionObject(options) {
|
|
1674
|
+
const first = options[0];
|
|
1675
|
+
return typeof first === "object" && first !== null && !Array.isArray(first) ? first : {};
|
|
1676
|
+
}
|
|
1677
|
+
/** A string-valued key of the first option object, or undefined when absent or not a string. */
|
|
1678
|
+
function stringOption(options, key) {
|
|
1679
|
+
const value = firstOptionObject(options)[key];
|
|
1680
|
+
return typeof value === "string" ? value : void 0;
|
|
1681
|
+
}
|
|
1682
|
+
//#endregion
|
|
1683
|
+
//#region src/rules/no-hardcoded-jsx-text.ts
|
|
1684
|
+
const defaultWordPattern = String.raw`\p{L}{3,}`;
|
|
1685
|
+
/** A string literal that is the whole expression of `{...}` placed among an element's children. */
|
|
1686
|
+
function isChildStringLiteral(node) {
|
|
1687
|
+
const { expression, parent } = node;
|
|
1688
|
+
if (expression.type !== "Literal" || typeof expression.value !== "string") return false;
|
|
1689
|
+
return parent.type === "JSXElement" || parent.type === "JSXFragment";
|
|
1690
|
+
}
|
|
1691
|
+
/** Reject words written straight into JSX instead of coming from the translation dictionary. */
|
|
1692
|
+
const noHardcodedJsxTextRule = defineRule({
|
|
1693
|
+
meta: {
|
|
1694
|
+
type: "problem",
|
|
1695
|
+
docs: { description: "Disallow literal words in JSX children; every string on screen comes from the translation dictionary." },
|
|
1696
|
+
messages: { hardcodedText: "Do not write words straight into JSX. Every string on screen comes from the translation dictionary." },
|
|
1697
|
+
schema: [{
|
|
1698
|
+
type: "object",
|
|
1699
|
+
properties: { pattern: { type: "string" } },
|
|
1700
|
+
additionalProperties: false
|
|
1701
|
+
}]
|
|
1702
|
+
},
|
|
1703
|
+
create(context) {
|
|
1704
|
+
const word = new RegExp(stringOption(context.options, "pattern") ?? defaultWordPattern, "u");
|
|
1705
|
+
return {
|
|
1706
|
+
JSXText(node) {
|
|
1707
|
+
if (word.test(node.value)) context.report({
|
|
1708
|
+
node,
|
|
1709
|
+
messageId: "hardcodedText"
|
|
1710
|
+
});
|
|
1711
|
+
},
|
|
1712
|
+
JSXExpressionContainer(node) {
|
|
1713
|
+
if (isChildStringLiteral(node) && word.test(node.expression.value)) context.report({
|
|
1714
|
+
node: node.expression,
|
|
1715
|
+
messageId: "hardcodedText"
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/rules/no-use-effect.ts
|
|
1723
|
+
const effectHooks = /* @__PURE__ */ new Set(["useEffect", "useLayoutEffect"]);
|
|
1724
|
+
/** The name a member expression reads, written `.useEffect` or `["useEffect"]`; null when the key is not a literal name. */
|
|
1725
|
+
function memberPropertyName(node) {
|
|
1726
|
+
const { property } = node;
|
|
1727
|
+
if (!node.computed) return property.type === "Identifier" ? property.name : null;
|
|
1728
|
+
return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
|
|
1729
|
+
}
|
|
1730
|
+
/** The effect hook a call reaches, called bare (`useEffect(...)`) or through a member (`React.useEffect(...)`, `React["useEffect"](...)`). */
|
|
1731
|
+
function calledEffectHook(callee) {
|
|
1732
|
+
if (callee.type === "Identifier") return effectHooks.has(callee.name) ? callee.name : null;
|
|
1733
|
+
if (callee.type !== "MemberExpression") return null;
|
|
1734
|
+
const name = memberPropertyName(callee);
|
|
1735
|
+
return name !== null && effectHooks.has(name) ? name : null;
|
|
1736
|
+
}
|
|
1737
|
+
function importedName(specifier) {
|
|
1738
|
+
return specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
1739
|
+
}
|
|
1740
|
+
function isTypeOnly(specifier) {
|
|
1741
|
+
const declaration = specifier.parent;
|
|
1742
|
+
return specifier.importKind === "type" || declaration.type === "ImportDeclaration" && declaration.importKind === "type";
|
|
1743
|
+
}
|
|
1744
|
+
/** Reject `useEffect` and `useLayoutEffect`, called or imported. */
|
|
1745
|
+
const noUseEffectRule = defineRule({
|
|
1746
|
+
meta: {
|
|
1747
|
+
type: "problem",
|
|
1748
|
+
docs: { description: "Disallow useEffect and useLayoutEffect; derive during render, act in the event handler, or subscribe through a reactive query." },
|
|
1749
|
+
messages: {
|
|
1750
|
+
noEffect: "Do not call {{name}}. Derive the value during render, do the work in the event handler, or subscribe through a reactive query.",
|
|
1751
|
+
noEffectWithHint: "Do not call {{name}}. Derive the value during render, do the work in the event handler, or subscribe through a reactive query. {{hint}}"
|
|
1752
|
+
},
|
|
1753
|
+
schema: [{
|
|
1754
|
+
type: "object",
|
|
1755
|
+
properties: { hint: { type: "string" } },
|
|
1756
|
+
additionalProperties: false
|
|
1757
|
+
}]
|
|
1758
|
+
},
|
|
1759
|
+
create(context) {
|
|
1760
|
+
const hint = stringOption(context.options, "hint");
|
|
1761
|
+
const report = (node, name) => {
|
|
1762
|
+
if (hint === void 0) context.report({
|
|
1763
|
+
node,
|
|
1764
|
+
messageId: "noEffect",
|
|
1765
|
+
data: { name }
|
|
1766
|
+
});
|
|
1767
|
+
else context.report({
|
|
1768
|
+
node,
|
|
1769
|
+
messageId: "noEffectWithHint",
|
|
1770
|
+
data: {
|
|
1771
|
+
name,
|
|
1772
|
+
hint
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
};
|
|
1776
|
+
return {
|
|
1777
|
+
CallExpression(node) {
|
|
1778
|
+
const name = calledEffectHook(node.callee);
|
|
1779
|
+
if (name !== null) report(node, name);
|
|
1780
|
+
},
|
|
1781
|
+
ImportSpecifier(node) {
|
|
1782
|
+
if (isTypeOnly(node)) return;
|
|
1783
|
+
const name = importedName(node);
|
|
1784
|
+
if (effectHooks.has(name)) report(node, name);
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/rules/static-public-env.ts
|
|
1791
|
+
/** The name a member expression reads, the way Babel's `toComputedKey` does: `.NAME` and `["NAME"]` only. */
|
|
1792
|
+
function literalKeyName(node) {
|
|
1793
|
+
const { property } = node;
|
|
1794
|
+
if (!node.computed) return property.type === "Identifier" ? property.name : null;
|
|
1795
|
+
return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
|
|
1796
|
+
}
|
|
1797
|
+
/** Whether a member expression is `process.env` or `process["env"]`, the object the inliner matches. */
|
|
1798
|
+
function isProcessEnv(node) {
|
|
1799
|
+
return node.object.type === "Identifier" && node.object.name === "process" && literalKeyName(node) === "env";
|
|
1800
|
+
}
|
|
1801
|
+
/** Whether a pattern takes `process.env` apart, in a declaration or in an assignment. */
|
|
1802
|
+
function isDestructured(node) {
|
|
1803
|
+
const { parent } = node;
|
|
1804
|
+
let target = null;
|
|
1805
|
+
if (parent.type === "VariableDeclarator" && parent.init === node) target = parent.id;
|
|
1806
|
+
if (parent.type === "AssignmentExpression" && parent.right === node) target = parent.left;
|
|
1807
|
+
return target !== null && (target.type === "ObjectPattern" || target.type === "ArrayPattern");
|
|
1808
|
+
}
|
|
1809
|
+
/** What is wrong with a `process.env.KEY` read, or null when Expo replaces that exact shape. */
|
|
1810
|
+
function readProblem(env, read) {
|
|
1811
|
+
const { parent } = read;
|
|
1812
|
+
if (parent.type === "AssignmentExpression" && parent.left === read) return "assigned";
|
|
1813
|
+
if (env.optional || read.optional) return "optionalChain";
|
|
1814
|
+
if (literalKeyName(read) !== null) return null;
|
|
1815
|
+
return read.property.type === "Literal" || read.property.type === "TemplateLiteral" ? "literalKey" : "computed";
|
|
1816
|
+
}
|
|
1817
|
+
/** Reject every use of `process.env` that Expo's textual inlining cannot see. */
|
|
1818
|
+
const staticPublicEnvRule = defineRule({
|
|
1819
|
+
meta: {
|
|
1820
|
+
type: "problem",
|
|
1821
|
+
docs: { description: "Allow only the `process.env` reads Expo replaces at bundle time; everything else is undefined on the device." },
|
|
1822
|
+
messages: {
|
|
1823
|
+
destructured: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so there is no `process.env` object to destructure on the device. Read each variable as `process.env.NAME`.",
|
|
1824
|
+
computed: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so a key it has to evaluate is undefined on the device. Read the variable as `process.env.NAME` with the name spelled out.",
|
|
1825
|
+
literalKey: "Expo reads a computed key only when it is a string literal, so a template or a number leaves the read in the bundle and it is undefined on the device. Read the variable as `process.env.NAME`.",
|
|
1826
|
+
optionalChain: "Expo's inliner never sees an optional chain, so `process.env?.NAME` survives into the bundle and is undefined on the device. Read the variable as `process.env.NAME`.",
|
|
1827
|
+
assigned: "Expo replaces reads, not writes: an assignment to `process.env` stays in the bundle and no inlined read will ever see it. Set the variable in the environment the bundler reads.",
|
|
1828
|
+
bareValue: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so `process.env` passed around as a value is empty on the device. Read each variable as `process.env.NAME`."
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
create(context) {
|
|
1832
|
+
return { MemberExpression(node) {
|
|
1833
|
+
if (!isProcessEnv(node)) return;
|
|
1834
|
+
const { parent } = node;
|
|
1835
|
+
if (parent.type === "MemberExpression" && parent.object === node) {
|
|
1836
|
+
const problem = readProblem(node, parent);
|
|
1837
|
+
if (problem !== null) context.report({
|
|
1838
|
+
node: parent,
|
|
1839
|
+
messageId: problem
|
|
1840
|
+
});
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (isDestructured(node)) {
|
|
1844
|
+
context.report({
|
|
1845
|
+
node: parent,
|
|
1846
|
+
messageId: "destructured"
|
|
1847
|
+
});
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
context.report({
|
|
1851
|
+
node,
|
|
1852
|
+
messageId: "bareValue"
|
|
1853
|
+
});
|
|
1854
|
+
} };
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region src/index.ts
|
|
1859
|
+
const rdjperronPlugin = eslintCompatPlugin({
|
|
1860
|
+
meta: { name: "rdjperron" },
|
|
1671
1861
|
rules: {
|
|
1672
1862
|
"no-array-filter-map": noArrayFilterMapRule,
|
|
1673
|
-
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
1674
1863
|
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
1675
1864
|
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
1865
|
+
"no-hardcoded-jsx-text": noHardcodedJsxTextRule,
|
|
1676
1866
|
"no-known-value-widening": noKnownValueWideningRule,
|
|
1677
1867
|
"no-module-mocking": noModuleMockingRule,
|
|
1678
1868
|
"no-object-parameters": noObjectParametersRule,
|
|
1869
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
1679
1870
|
"no-reflect-apply": noReflectApplyRule,
|
|
1680
1871
|
"no-reflect-get": noReflectGetRule,
|
|
1681
1872
|
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
1682
|
-
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
1683
1873
|
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
1684
1874
|
"no-unknown-parameters": noUnknownParametersRule,
|
|
1685
1875
|
"no-unknown-returns": noUnknownReturnsRule,
|
|
1686
1876
|
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
1877
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
1878
|
+
"no-use-effect": noUseEffectRule,
|
|
1687
1879
|
"no-widen-then-assert": noWidenThenAssertRule,
|
|
1688
|
-
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule
|
|
1880
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
|
|
1881
|
+
"static-public-env": staticPublicEnvRule
|
|
1689
1882
|
}
|
|
1690
1883
|
});
|
|
1691
1884
|
//#endregion
|
|
1692
|
-
export {
|
|
1885
|
+
export { rdjperronPlugin as default };
|
|
1693
1886
|
|
|
1694
|
-
//# sourceMappingURL=
|
|
1887
|
+
//# sourceMappingURL=index.mjs.map
|