@stll/oxlint-plugin 0.0.1-placeholder.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/dist/index.cjs +27 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +21 -0
- package/dist/index.mjs.map +1 -0
- package/dist/no-any-casts.cjs +30 -0
- package/dist/no-any-casts.cjs.map +1 -0
- package/dist/no-any-casts.d.cts +4 -0
- package/dist/no-any-casts.d.mts +5 -0
- package/dist/no-any-casts.mjs +27 -0
- package/dist/no-any-casts.mjs.map +1 -0
- package/dist/no-dangerous-type-assertions.cjs +40 -0
- package/dist/no-dangerous-type-assertions.cjs.map +1 -0
- package/dist/no-dangerous-type-assertions.d.cts +4 -0
- package/dist/no-dangerous-type-assertions.d.mts +5 -0
- package/dist/no-dangerous-type-assertions.mjs +37 -0
- package/dist/no-dangerous-type-assertions.mjs.map +1 -0
- package/dist/no-partial-record-satisfies.cjs +45 -0
- package/dist/no-partial-record-satisfies.cjs.map +1 -0
- package/dist/no-partial-record-satisfies.d.cts +4 -0
- package/dist/no-partial-record-satisfies.d.mts +5 -0
- package/dist/no-partial-record-satisfies.mjs +41 -0
- package/dist/no-partial-record-satisfies.mjs.map +1 -0
- package/dist/no-unsafe-inner-html.cjs +123 -0
- package/dist/no-unsafe-inner-html.cjs.map +1 -0
- package/dist/no-unsafe-inner-html.d.cts +4 -0
- package/dist/no-unsafe-inner-html.d.mts +5 -0
- package/dist/no-unsafe-inner-html.mjs +119 -0
- package/dist/no-unsafe-inner-html.mjs.map +1 -0
- package/dist/utils.cjs +54 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.mjs +31 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +95 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
const noAnyCastsPluginSpecifier = "@stll/oxlint-plugin/no-any-casts";
|
|
4
|
+
const noDangerousTypeAssertionsPluginSpecifier = "@stll/oxlint-plugin/no-dangerous-type-assertions";
|
|
5
|
+
const noPartialRecordSatisfiesPluginSpecifier = "@stll/oxlint-plugin/no-partial-record-satisfies";
|
|
6
|
+
const noUnsafeInnerHtmlPluginSpecifier = "@stll/oxlint-plugin/no-unsafe-inner-html";
|
|
7
|
+
const portableSafetyPluginSpecifiers = [
|
|
8
|
+
noAnyCastsPluginSpecifier,
|
|
9
|
+
noDangerousTypeAssertionsPluginSpecifier,
|
|
10
|
+
noPartialRecordSatisfiesPluginSpecifier,
|
|
11
|
+
noUnsafeInnerHtmlPluginSpecifier
|
|
12
|
+
];
|
|
13
|
+
const portableSafetyRules = {
|
|
14
|
+
"no-any-casts/no-any-casts": "error",
|
|
15
|
+
"no-dangerous-type-assertions/no-dangerous-type-assertions": "error",
|
|
16
|
+
"no-partial-record-satisfies/no-partial-record-satisfies": "error",
|
|
17
|
+
"no-unsafe-inner-html/no-unsafe-inner-html": "error"
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
exports.noAnyCastsPluginSpecifier = noAnyCastsPluginSpecifier;
|
|
21
|
+
exports.noDangerousTypeAssertionsPluginSpecifier = noDangerousTypeAssertionsPluginSpecifier;
|
|
22
|
+
exports.noPartialRecordSatisfiesPluginSpecifier = noPartialRecordSatisfiesPluginSpecifier;
|
|
23
|
+
exports.noUnsafeInnerHtmlPluginSpecifier = noUnsafeInnerHtmlPluginSpecifier;
|
|
24
|
+
exports.portableSafetyPluginSpecifiers = portableSafetyPluginSpecifiers;
|
|
25
|
+
exports.portableSafetyRules = portableSafetyRules;
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { OxlintConfig } from \"oxlint\";\n\ntype JsPlugins = NonNullable<OxlintConfig[\"jsPlugins\"]>;\ntype Rules = NonNullable<OxlintConfig[\"rules\"]>;\n\nexport const noAnyCastsPluginSpecifier = \"@stll/oxlint-plugin/no-any-casts\";\nexport const noDangerousTypeAssertionsPluginSpecifier =\n \"@stll/oxlint-plugin/no-dangerous-type-assertions\";\nexport const noPartialRecordSatisfiesPluginSpecifier =\n \"@stll/oxlint-plugin/no-partial-record-satisfies\";\nexport const noUnsafeInnerHtmlPluginSpecifier =\n \"@stll/oxlint-plugin/no-unsafe-inner-html\";\n\nexport const portableSafetyPluginSpecifiers = [\n noAnyCastsPluginSpecifier,\n noDangerousTypeAssertionsPluginSpecifier,\n noPartialRecordSatisfiesPluginSpecifier,\n noUnsafeInnerHtmlPluginSpecifier,\n] satisfies JsPlugins;\n\nexport const portableSafetyRules = {\n \"no-any-casts/no-any-casts\": \"error\",\n \"no-dangerous-type-assertions/no-dangerous-type-assertions\": \"error\",\n \"no-partial-record-satisfies/no-partial-record-satisfies\": \"error\",\n \"no-unsafe-inner-html/no-unsafe-inner-html\": \"error\",\n} satisfies Rules;\n"],"mappings":";;AAKA,MAAa,4BAA4B;AACzC,MAAa,2CACX;AACF,MAAa,0CACX;AACF,MAAa,mCACX;AAEF,MAAa,iCAAiC;CAC5C;CACA;CACA;CACA;AACF;AAEA,MAAa,sBAAsB;CACjC,6BAA6B;CAC7B,6DAA6D;CAC7D,2DAA2D;CAC3D,6CAA6C;AAC/C"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
declare const noAnyCastsPluginSpecifier = "@stll/oxlint-plugin/no-any-casts";
|
|
3
|
+
declare const noDangerousTypeAssertionsPluginSpecifier = "@stll/oxlint-plugin/no-dangerous-type-assertions";
|
|
4
|
+
declare const noPartialRecordSatisfiesPluginSpecifier = "@stll/oxlint-plugin/no-partial-record-satisfies";
|
|
5
|
+
declare const noUnsafeInnerHtmlPluginSpecifier = "@stll/oxlint-plugin/no-unsafe-inner-html";
|
|
6
|
+
declare const portableSafetyPluginSpecifiers: string[];
|
|
7
|
+
declare const portableSafetyRules: {
|
|
8
|
+
"no-any-casts/no-any-casts": "error";
|
|
9
|
+
"no-dangerous-type-assertions/no-dangerous-type-assertions": "error";
|
|
10
|
+
"no-partial-record-satisfies/no-partial-record-satisfies": "error";
|
|
11
|
+
"no-unsafe-inner-html/no-unsafe-inner-html": "error";
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { noAnyCastsPluginSpecifier, noDangerousTypeAssertionsPluginSpecifier, noPartialRecordSatisfiesPluginSpecifier, noUnsafeInnerHtmlPluginSpecifier, portableSafetyPluginSpecifiers, portableSafetyRules };
|
|
15
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
declare const noAnyCastsPluginSpecifier = "@stll/oxlint-plugin/no-any-casts";
|
|
3
|
+
declare const noDangerousTypeAssertionsPluginSpecifier = "@stll/oxlint-plugin/no-dangerous-type-assertions";
|
|
4
|
+
declare const noPartialRecordSatisfiesPluginSpecifier = "@stll/oxlint-plugin/no-partial-record-satisfies";
|
|
5
|
+
declare const noUnsafeInnerHtmlPluginSpecifier = "@stll/oxlint-plugin/no-unsafe-inner-html";
|
|
6
|
+
declare const portableSafetyPluginSpecifiers: string[];
|
|
7
|
+
declare const portableSafetyRules: {
|
|
8
|
+
"no-any-casts/no-any-casts": "error";
|
|
9
|
+
"no-dangerous-type-assertions/no-dangerous-type-assertions": "error";
|
|
10
|
+
"no-partial-record-satisfies/no-partial-record-satisfies": "error";
|
|
11
|
+
"no-unsafe-inner-html/no-unsafe-inner-html": "error";
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { noAnyCastsPluginSpecifier, noDangerousTypeAssertionsPluginSpecifier, noPartialRecordSatisfiesPluginSpecifier, noUnsafeInnerHtmlPluginSpecifier, portableSafetyPluginSpecifiers, portableSafetyRules };
|
|
15
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
const noAnyCastsPluginSpecifier = "@stll/oxlint-plugin/no-any-casts";
|
|
3
|
+
const noDangerousTypeAssertionsPluginSpecifier = "@stll/oxlint-plugin/no-dangerous-type-assertions";
|
|
4
|
+
const noPartialRecordSatisfiesPluginSpecifier = "@stll/oxlint-plugin/no-partial-record-satisfies";
|
|
5
|
+
const noUnsafeInnerHtmlPluginSpecifier = "@stll/oxlint-plugin/no-unsafe-inner-html";
|
|
6
|
+
const portableSafetyPluginSpecifiers = [
|
|
7
|
+
noAnyCastsPluginSpecifier,
|
|
8
|
+
noDangerousTypeAssertionsPluginSpecifier,
|
|
9
|
+
noPartialRecordSatisfiesPluginSpecifier,
|
|
10
|
+
noUnsafeInnerHtmlPluginSpecifier
|
|
11
|
+
];
|
|
12
|
+
const portableSafetyRules = {
|
|
13
|
+
"no-any-casts/no-any-casts": "error",
|
|
14
|
+
"no-dangerous-type-assertions/no-dangerous-type-assertions": "error",
|
|
15
|
+
"no-partial-record-satisfies/no-partial-record-satisfies": "error",
|
|
16
|
+
"no-unsafe-inner-html/no-unsafe-inner-html": "error"
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { noAnyCastsPluginSpecifier, noDangerousTypeAssertionsPluginSpecifier, noPartialRecordSatisfiesPluginSpecifier, noUnsafeInnerHtmlPluginSpecifier, portableSafetyPluginSpecifiers, portableSafetyRules };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { OxlintConfig } from \"oxlint\";\n\ntype JsPlugins = NonNullable<OxlintConfig[\"jsPlugins\"]>;\ntype Rules = NonNullable<OxlintConfig[\"rules\"]>;\n\nexport const noAnyCastsPluginSpecifier = \"@stll/oxlint-plugin/no-any-casts\";\nexport const noDangerousTypeAssertionsPluginSpecifier =\n \"@stll/oxlint-plugin/no-dangerous-type-assertions\";\nexport const noPartialRecordSatisfiesPluginSpecifier =\n \"@stll/oxlint-plugin/no-partial-record-satisfies\";\nexport const noUnsafeInnerHtmlPluginSpecifier =\n \"@stll/oxlint-plugin/no-unsafe-inner-html\";\n\nexport const portableSafetyPluginSpecifiers = [\n noAnyCastsPluginSpecifier,\n noDangerousTypeAssertionsPluginSpecifier,\n noPartialRecordSatisfiesPluginSpecifier,\n noUnsafeInnerHtmlPluginSpecifier,\n] satisfies JsPlugins;\n\nexport const portableSafetyRules = {\n \"no-any-casts/no-any-casts\": \"error\",\n \"no-dangerous-type-assertions/no-dangerous-type-assertions\": \"error\",\n \"no-partial-record-satisfies/no-partial-record-satisfies\": \"error\",\n \"no-unsafe-inner-html/no-unsafe-inner-html\": \"error\",\n} satisfies Rules;\n"],"mappings":";AAKA,MAAa,4BAA4B;AACzC,MAAa,2CACX;AACF,MAAa,0CACX;AACF,MAAa,mCACX;AAEF,MAAa,iCAAiC;CAC5C;CACA;CACA;CACA;AACF;AAEA,MAAa,sBAAsB;CACjC,6BAA6B;CAC7B,6DAA6D;CAC7D,2DAA2D;CAC3D,6CAA6C;AAC/C"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region src/no-any-casts.ts
|
|
6
|
+
var no_any_casts_default = (0, require("@oxlint/plugins").eslintCompatPlugin)({
|
|
7
|
+
meta: { name: "no-any-casts" },
|
|
8
|
+
rules: { "no-any-casts": {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
messages: { noAnyCast: "Avoid `as any` casts. They bypass branded IDs and discriminated-union narrowing. Narrow with a type guard, use `unknown` + a guard, or refactor the source so the cast isn't needed." }
|
|
12
|
+
},
|
|
13
|
+
createOnce(context) {
|
|
14
|
+
function check(node) {
|
|
15
|
+
if (node.typeAnnotation?.type === "TSAnyKeyword") context.report({
|
|
16
|
+
node,
|
|
17
|
+
messageId: "noAnyCast"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
TSAsExpression: check,
|
|
22
|
+
TSTypeAssertion: check
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
} }
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
exports.default = no_any_casts_default;
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=no-any-casts.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-any-casts.cjs","names":[],"sources":["../src/no-any-casts.ts"],"sourcesContent":["// Disallow `as any` and `<any>` casts.\n//\n// Distinct from `no-explicit-any` (which targets `: any` annotations\n// in declarations). This rule catches the cast form — typically\n// `value as any` or the laundering `value as any as Target` — which\n// bypasses brand checks, discriminated-union narrowing, and every\n// other safety the type system provides.\n\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-any-casts\" },\n rules: {\n \"no-any-casts\": {\n meta: {\n type: \"problem\",\n messages: {\n noAnyCast:\n \"Avoid `as any` casts. They bypass branded IDs and \" +\n \"discriminated-union narrowing. Narrow with a type guard, \" +\n \"use `unknown` + a guard, or refactor the source so the \" +\n \"cast isn't needed.\",\n },\n },\n createOnce(context) {\n function check(node) {\n if (node.typeAnnotation?.type === \"TSAnyKeyword\") {\n context.report({ node, messageId: \"noAnyCast\" });\n }\n }\n return {\n TSAsExpression: check,\n TSTypeAssertion: check,\n };\n },\n },\n },\n});\n"],"mappings":";;;;;AAUA,IAAA,wBAAA,4BAAA,CAAA,CAAA,mBAAA,CAAkC;CAChC,MAAM,EAAE,MAAM,eAAe;CAC7B,OAAO,EACL,gBAAgB;EACd,MAAM;GACJ,MAAM;GACN,UAAU,EACR,WACE,uLAIJ;EACF;EACA,WAAW,SAAS;GAClB,SAAS,MAAM,MAAM;IACnB,IAAI,KAAK,gBAAgB,SAAS,gBAChC,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAY,CAAC;GAEnD;GACA,OAAO;IACL,gBAAgB;IAChB,iBAAiB;GACnB;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
//#region src/no-any-casts.ts
|
|
3
|
+
var no_any_casts_default = eslintCompatPlugin({
|
|
4
|
+
meta: { name: "no-any-casts" },
|
|
5
|
+
rules: { "no-any-casts": {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
messages: { noAnyCast: "Avoid `as any` casts. They bypass branded IDs and discriminated-union narrowing. Narrow with a type guard, use `unknown` + a guard, or refactor the source so the cast isn't needed." }
|
|
9
|
+
},
|
|
10
|
+
createOnce(context) {
|
|
11
|
+
function check(node) {
|
|
12
|
+
if (node.typeAnnotation?.type === "TSAnyKeyword") context.report({
|
|
13
|
+
node,
|
|
14
|
+
messageId: "noAnyCast"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
TSAsExpression: check,
|
|
19
|
+
TSTypeAssertion: check
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
} }
|
|
23
|
+
});
|
|
24
|
+
//#endregion
|
|
25
|
+
export { no_any_casts_default as default };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=no-any-casts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-any-casts.mjs","names":[],"sources":["../src/no-any-casts.ts"],"sourcesContent":["// Disallow `as any` and `<any>` casts.\n//\n// Distinct from `no-explicit-any` (which targets `: any` annotations\n// in declarations). This rule catches the cast form — typically\n// `value as any` or the laundering `value as any as Target` — which\n// bypasses brand checks, discriminated-union narrowing, and every\n// other safety the type system provides.\n\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-any-casts\" },\n rules: {\n \"no-any-casts\": {\n meta: {\n type: \"problem\",\n messages: {\n noAnyCast:\n \"Avoid `as any` casts. They bypass branded IDs and \" +\n \"discriminated-union narrowing. Narrow with a type guard, \" +\n \"use `unknown` + a guard, or refactor the source so the \" +\n \"cast isn't needed.\",\n },\n },\n createOnce(context) {\n function check(node) {\n if (node.typeAnnotation?.type === \"TSAnyKeyword\") {\n context.report({ node, messageId: \"noAnyCast\" });\n }\n }\n return {\n TSAsExpression: check,\n TSTypeAssertion: check,\n };\n },\n },\n },\n});\n"],"mappings":";;AAUA,IAAA,uBAAe,mBAAmB;CAChC,MAAM,EAAE,MAAM,eAAe;CAC7B,OAAO,EACL,gBAAgB;EACd,MAAM;GACJ,MAAM;GACN,UAAU,EACR,WACE,uLAIJ;EACF;EACA,WAAW,SAAS;GAClB,SAAS,MAAM,MAAM;IACnB,IAAI,KAAK,gBAAgB,SAAS,gBAChC,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAY,CAAC;GAEnD;GACA,OAAO;IACL,gBAAgB;IAChB,iBAAiB;GACnB;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region src/no-dangerous-type-assertions.ts
|
|
6
|
+
var no_dangerous_type_assertions_default = (0, require("@oxlint/plugins").eslintCompatPlugin)({
|
|
7
|
+
meta: { name: "no-dangerous-type-assertions" },
|
|
8
|
+
rules: { "no-dangerous-type-assertions": {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
messages: { noObjectLiteralCast: "Don't cast an object literal with `as`. Type the binding (`const x: T = { ... }`) or use `satisfies T` so missing required fields fail typecheck." }
|
|
12
|
+
},
|
|
13
|
+
createOnce(context) {
|
|
14
|
+
function unwrapExpression(expression) {
|
|
15
|
+
let current = expression;
|
|
16
|
+
while (current?.type === "ParenthesizedExpression" || current?.type === "TSAsExpression" || current?.type === "TSTypeAssertion" || current?.type === "TSNonNullExpression" || current?.type === "TSSatisfiesExpression") current = current.expression;
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
function check(node) {
|
|
20
|
+
if (unwrapExpression(node.expression)?.type !== "ObjectExpression") return;
|
|
21
|
+
const ann = node.typeAnnotation;
|
|
22
|
+
if (!ann) return;
|
|
23
|
+
if (ann.type === "TSTypeReference" && ann.typeName?.type === "Identifier" && ann.typeName.name === "const") return;
|
|
24
|
+
if (ann.type === "TSAnyKeyword" || ann.type === "TSUnknownKeyword") return;
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: "noObjectLiteralCast"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
TSAsExpression: check,
|
|
32
|
+
TSTypeAssertion: check
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
} }
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
exports.default = no_dangerous_type_assertions_default;
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=no-dangerous-type-assertions.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-dangerous-type-assertions.cjs","names":[],"sources":["../src/no-dangerous-type-assertions.ts"],"sourcesContent":["// Disallow object-literal casts: `{...} as T`, `<T>{...}`.\n//\n// Casting an object literal to a type silently hides missing required\n// fields — the type system would otherwise catch the omission. Use a\n// typed binding (`const x: T = { ... }`) or `satisfies T` so missing\n// fields surface as type errors.\n//\n// Allows `as const` (legitimate widening control).\n// Allows `as any` / `as unknown` (those have dedicated rules / are\n// often used as the first step of an explicit launder).\n\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-dangerous-type-assertions\" },\n rules: {\n \"no-dangerous-type-assertions\": {\n meta: {\n type: \"problem\",\n messages: {\n noObjectLiteralCast:\n \"Don't cast an object literal with `as`. Type the binding \" +\n \"(`const x: T = { ... }`) or use `satisfies T` so missing \" +\n \"required fields fail typecheck.\",\n },\n },\n createOnce(context) {\n function unwrapExpression(expression) {\n let current = expression;\n while (\n current?.type === \"ParenthesizedExpression\" ||\n current?.type === \"TSAsExpression\" ||\n current?.type === \"TSTypeAssertion\" ||\n current?.type === \"TSNonNullExpression\" ||\n current?.type === \"TSSatisfiesExpression\"\n ) {\n current = current.expression;\n }\n return current;\n }\n\n function check(node) {\n const expression = unwrapExpression(node.expression);\n if (expression?.type !== \"ObjectExpression\") {\n return;\n }\n const ann = node.typeAnnotation;\n if (!ann) {\n return;\n }\n // Allow `as const`\n if (\n ann.type === \"TSTypeReference\" &&\n ann.typeName?.type === \"Identifier\" &&\n ann.typeName.name === \"const\"\n ) {\n return;\n }\n // `as any` and `as unknown` have their own rules / are\n // typically the laundering step, not the final shape claim.\n if (ann.type === \"TSAnyKeyword\" || ann.type === \"TSUnknownKeyword\") {\n return;\n }\n context.report({ node, messageId: \"noObjectLiteralCast\" });\n }\n return {\n TSAsExpression: check,\n TSTypeAssertion: check,\n };\n },\n },\n },\n});\n"],"mappings":";;;;;AAaA,IAAA,wCAAA,4BAAA,CAAA,CAAA,mBAAA,CAAkC;CAChC,MAAM,EAAE,MAAM,+BAA+B;CAC7C,OAAO,EACL,gCAAgC;EAC9B,MAAM;GACJ,MAAM;GACN,UAAU,EACR,qBACE,oJAGJ;EACF;EACA,WAAW,SAAS;GAClB,SAAS,iBAAiB,YAAY;IACpC,IAAI,UAAU;IACd,OACE,SAAS,SAAS,6BAClB,SAAS,SAAS,oBAClB,SAAS,SAAS,qBAClB,SAAS,SAAS,yBAClB,SAAS,SAAS,yBAElB,UAAU,QAAQ;IAEpB,OAAO;GACT;GAEA,SAAS,MAAM,MAAM;IAEnB,IADmB,iBAAiB,KAAK,UAC5B,CAAC,EAAE,SAAS,oBACvB;IAEF,MAAM,MAAM,KAAK;IACjB,IAAI,CAAC,KACH;IAGF,IACE,IAAI,SAAS,qBACb,IAAI,UAAU,SAAS,gBACvB,IAAI,SAAS,SAAS,SAEtB;IAIF,IAAI,IAAI,SAAS,kBAAkB,IAAI,SAAS,oBAC9C;IAEF,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAsB,CAAC;GAC3D;GACA,OAAO;IACL,gBAAgB;IAChB,iBAAiB;GACnB;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
//#region src/no-dangerous-type-assertions.ts
|
|
3
|
+
var no_dangerous_type_assertions_default = eslintCompatPlugin({
|
|
4
|
+
meta: { name: "no-dangerous-type-assertions" },
|
|
5
|
+
rules: { "no-dangerous-type-assertions": {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
messages: { noObjectLiteralCast: "Don't cast an object literal with `as`. Type the binding (`const x: T = { ... }`) or use `satisfies T` so missing required fields fail typecheck." }
|
|
9
|
+
},
|
|
10
|
+
createOnce(context) {
|
|
11
|
+
function unwrapExpression(expression) {
|
|
12
|
+
let current = expression;
|
|
13
|
+
while (current?.type === "ParenthesizedExpression" || current?.type === "TSAsExpression" || current?.type === "TSTypeAssertion" || current?.type === "TSNonNullExpression" || current?.type === "TSSatisfiesExpression") current = current.expression;
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function check(node) {
|
|
17
|
+
if (unwrapExpression(node.expression)?.type !== "ObjectExpression") return;
|
|
18
|
+
const ann = node.typeAnnotation;
|
|
19
|
+
if (!ann) return;
|
|
20
|
+
if (ann.type === "TSTypeReference" && ann.typeName?.type === "Identifier" && ann.typeName.name === "const") return;
|
|
21
|
+
if (ann.type === "TSAnyKeyword" || ann.type === "TSUnknownKeyword") return;
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
messageId: "noObjectLiteralCast"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
TSAsExpression: check,
|
|
29
|
+
TSTypeAssertion: check
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
} }
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { no_dangerous_type_assertions_default as default };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=no-dangerous-type-assertions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-dangerous-type-assertions.mjs","names":[],"sources":["../src/no-dangerous-type-assertions.ts"],"sourcesContent":["// Disallow object-literal casts: `{...} as T`, `<T>{...}`.\n//\n// Casting an object literal to a type silently hides missing required\n// fields — the type system would otherwise catch the omission. Use a\n// typed binding (`const x: T = { ... }`) or `satisfies T` so missing\n// fields surface as type errors.\n//\n// Allows `as const` (legitimate widening control).\n// Allows `as any` / `as unknown` (those have dedicated rules / are\n// often used as the first step of an explicit launder).\n\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-dangerous-type-assertions\" },\n rules: {\n \"no-dangerous-type-assertions\": {\n meta: {\n type: \"problem\",\n messages: {\n noObjectLiteralCast:\n \"Don't cast an object literal with `as`. Type the binding \" +\n \"(`const x: T = { ... }`) or use `satisfies T` so missing \" +\n \"required fields fail typecheck.\",\n },\n },\n createOnce(context) {\n function unwrapExpression(expression) {\n let current = expression;\n while (\n current?.type === \"ParenthesizedExpression\" ||\n current?.type === \"TSAsExpression\" ||\n current?.type === \"TSTypeAssertion\" ||\n current?.type === \"TSNonNullExpression\" ||\n current?.type === \"TSSatisfiesExpression\"\n ) {\n current = current.expression;\n }\n return current;\n }\n\n function check(node) {\n const expression = unwrapExpression(node.expression);\n if (expression?.type !== \"ObjectExpression\") {\n return;\n }\n const ann = node.typeAnnotation;\n if (!ann) {\n return;\n }\n // Allow `as const`\n if (\n ann.type === \"TSTypeReference\" &&\n ann.typeName?.type === \"Identifier\" &&\n ann.typeName.name === \"const\"\n ) {\n return;\n }\n // `as any` and `as unknown` have their own rules / are\n // typically the laundering step, not the final shape claim.\n if (ann.type === \"TSAnyKeyword\" || ann.type === \"TSUnknownKeyword\") {\n return;\n }\n context.report({ node, messageId: \"noObjectLiteralCast\" });\n }\n return {\n TSAsExpression: check,\n TSTypeAssertion: check,\n };\n },\n },\n },\n});\n"],"mappings":";;AAaA,IAAA,uCAAe,mBAAmB;CAChC,MAAM,EAAE,MAAM,+BAA+B;CAC7C,OAAO,EACL,gCAAgC;EAC9B,MAAM;GACJ,MAAM;GACN,UAAU,EACR,qBACE,oJAGJ;EACF;EACA,WAAW,SAAS;GAClB,SAAS,iBAAiB,YAAY;IACpC,IAAI,UAAU;IACd,OACE,SAAS,SAAS,6BAClB,SAAS,SAAS,oBAClB,SAAS,SAAS,qBAClB,SAAS,SAAS,yBAClB,SAAS,SAAS,yBAElB,UAAU,QAAQ;IAEpB,OAAO;GACT;GAEA,SAAS,MAAM,MAAM;IAEnB,IADmB,iBAAiB,KAAK,UAC5B,CAAC,EAAE,SAAS,oBACvB;IAEF,MAAM,MAAM,KAAK;IACjB,IAAI,CAAC,KACH;IAGF,IACE,IAAI,SAAS,qBACb,IAAI,UAAU,SAAS,gBACvB,IAAI,SAAS,SAAS,SAEtB;IAIF,IAAI,IAAI,SAAS,kBAAkB,IAAI,SAAS,oBAC9C;IAEF,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAsB,CAAC;GAC3D;GACA,OAAO;IACL,gBAAgB;IAChB,iBAAiB;GACnB;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
const require_utils = require("./utils.cjs");
|
|
6
|
+
let _oxlint_plugins = require("@oxlint/plugins");
|
|
7
|
+
//#region src/no-partial-record-satisfies.ts
|
|
8
|
+
const unwrapReadonlyType = (typeNode) => {
|
|
9
|
+
if (typeNode?.type === "TSTypeReference" && require_utils.isIdentifier(typeNode.typeName, "Readonly")) return typeNode.typeArguments?.params?.at(0) ?? null;
|
|
10
|
+
return typeNode;
|
|
11
|
+
};
|
|
12
|
+
const isPartialRecordType = (typeNode) => {
|
|
13
|
+
const outer = unwrapReadonlyType(typeNode);
|
|
14
|
+
if (outer?.type !== "TSTypeReference" || !require_utils.isIdentifier(outer.typeName, "Partial")) return false;
|
|
15
|
+
const inner = unwrapReadonlyType(outer.typeArguments?.params?.at(0) ?? null);
|
|
16
|
+
return inner?.type === "TSTypeReference" && require_utils.isIdentifier(inner.typeName, "Record");
|
|
17
|
+
};
|
|
18
|
+
const isObjectLiteralExpression = (node) => {
|
|
19
|
+
if (node?.type === "ObjectExpression") return true;
|
|
20
|
+
if (node?.type === "TSAsExpression" && node.typeAnnotation?.type === "TSTypeReference" && require_utils.isIdentifier(node.typeAnnotation.typeName, "const")) return isObjectLiteralExpression(node.expression);
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
var no_partial_record_satisfies_default = (0, _oxlint_plugins.eslintCompatPlugin)({
|
|
24
|
+
meta: { name: "no-partial-record-satisfies" },
|
|
25
|
+
rules: { "no-partial-record-satisfies": {
|
|
26
|
+
meta: {
|
|
27
|
+
type: "problem",
|
|
28
|
+
messages: { noPartialRecordSatisfies: "Don't pin an object literal with `satisfies Partial<Record<...>>` — Partial cancels the totality check satisfies would otherwise give you, so a union member can silently fall through. Make the record total (explicit 'none'/false/null per member), narrow the key union, or add an eslint-disable naming what absence means for genuinely sparse data." }
|
|
29
|
+
},
|
|
30
|
+
createOnce(context) {
|
|
31
|
+
return { TSSatisfiesExpression(node) {
|
|
32
|
+
if (!isPartialRecordType(node.typeAnnotation)) return;
|
|
33
|
+
if (!isObjectLiteralExpression(node.expression)) return;
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "noPartialRecordSatisfies"
|
|
37
|
+
});
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
} }
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
exports.default = no_partial_record_satisfies_default;
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=no-partial-record-satisfies.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-partial-record-satisfies.cjs","names":["isIdentifier"],"sources":["../src/no-partial-record-satisfies.ts"],"sourcesContent":["import { eslintCompatPlugin } from \"@oxlint/plugins\";\n// Ban `<object literal> satisfies Partial<Record<Union, T>>` (with or\n// without a leading `as const`).\n//\n// A const object literal pinned with `satisfies` against a `Record` keyed by\n// a union declares itself the classification of every union member —\n// `satisfies` fails typecheck the moment a new member is added and left\n// unhandled. Wrapping that `Record` in `Partial` cancels the guarantee: a\n// missing member typechecks silently, so the literal can drift out of sync\n// with the union with no compiler signal. That silent-drift surface can cause\n// a classification-table bug when a newly added member falls through a\n// `Partial<Record<...>>` lookup instead of failing typecheck.\n//\n// `Readonly<...>` wrapping either side (`Readonly<Partial<Record<...>>>` or\n// `Partial<Readonly<Record<...>>>`) is unwrapped before the check, since it\n// changes mutability, not totality.\n//\n// Only the `satisfies` expression on an object literal is flagged. Plain\n// type-annotation positions (`const x: Partial<Record<...>> = {...}`),\n// function parameters, and variable/return type annotations are exempt:\n// those are legitimate uses (override inputs, accumulators, genuinely sparse\n// data) and carry no compiler guarantee of totality to lose.\n//\n// Flagged:\n// const x = {\n// a: 1,\n// } as const satisfies Partial<Record<Union, number>>;\n// const y = { a: 1 } satisfies Partial<Record<Union, number>>;\n// const z = {\n// a: 1,\n// } as const satisfies Readonly<Partial<Record<Union, number>>>;\n//\n// Allowed:\n// const x = {\n// a: 1,\n// } as const satisfies Record<Union, number>;\n// const fn = (overrides: Partial<Record<Union, number>>) => overrides;\n// let acc: Partial<Record<Union, number>> = {};\n// type Sparse = Partial<Record<Union, number>>;\n//\n// Fix by making the record total over the union — an explicit \"none\" /\n// `false` / `null` value for members where nothing applies forces a new\n// union member to be classified before it typechecks. If only a subset of\n// the union is ever relevant, derive a narrower union for the keys instead\n// of falling back to `Partial`. For genuinely sparse data where absence has\n// documented meaning (e.g. an unbounded key space), add an eslint-disable\n// with a comment naming what absence means.\n\nimport { isIdentifier } from \"./utils.ts\";\n\nconst unwrapReadonlyType = (typeNode) => {\n if (\n typeNode?.type === \"TSTypeReference\" &&\n isIdentifier(typeNode.typeName, \"Readonly\")\n ) {\n return typeNode.typeArguments?.params?.at(0) ?? null;\n }\n return typeNode;\n};\n\n// Matches `Partial<Record<...>>`, unwrapping a `Readonly<...>` wrapper on\n// either the outer (`Readonly<Partial<Record<...>>>`) or inner\n// (`Partial<Readonly<Record<...>>>`) type.\nconst isPartialRecordType = (typeNode) => {\n const outer = unwrapReadonlyType(typeNode);\n if (\n outer?.type !== \"TSTypeReference\" ||\n !isIdentifier(outer.typeName, \"Partial\")\n ) {\n return false;\n }\n const inner = unwrapReadonlyType(outer.typeArguments?.params?.at(0) ?? null);\n return (\n inner?.type === \"TSTypeReference\" && isIdentifier(inner.typeName, \"Record\")\n );\n};\n\n// Matches an object literal, or an object literal wrapped in `as const`.\nconst isObjectLiteralExpression = (node) => {\n if (node?.type === \"ObjectExpression\") {\n return true;\n }\n if (\n node?.type === \"TSAsExpression\" &&\n node.typeAnnotation?.type === \"TSTypeReference\" &&\n isIdentifier(node.typeAnnotation.typeName, \"const\")\n ) {\n return isObjectLiteralExpression(node.expression);\n }\n return false;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-partial-record-satisfies\" },\n rules: {\n \"no-partial-record-satisfies\": {\n meta: {\n type: \"problem\",\n messages: {\n noPartialRecordSatisfies:\n \"Don't pin an object literal with `satisfies Partial<Record<...>>` \" +\n \"— Partial cancels the totality check satisfies would otherwise \" +\n \"give you, so a union member can silently fall through. Make the \" +\n \"record total (explicit 'none'/false/null per member), narrow the \" +\n \"key union, or add an eslint-disable naming what absence means \" +\n \"for genuinely sparse data.\",\n },\n },\n createOnce(context) {\n return {\n TSSatisfiesExpression(node) {\n if (!isPartialRecordType(node.typeAnnotation)) {\n return;\n }\n if (!isObjectLiteralExpression(node.expression)) {\n return;\n }\n context.report({ node, messageId: \"noPartialRecordSatisfies\" });\n },\n };\n },\n },\n },\n});\n"],"mappings":";;;;;;;AAkDA,MAAM,sBAAsB,aAAa;CACvC,IACE,UAAU,SAAS,qBACnBA,cAAAA,aAAa,SAAS,UAAU,UAAU,GAE1C,OAAO,SAAS,eAAe,QAAQ,GAAG,CAAC,KAAK;CAElD,OAAO;AACT;AAKA,MAAM,uBAAuB,aAAa;CACxC,MAAM,QAAQ,mBAAmB,QAAQ;CACzC,IACE,OAAO,SAAS,qBAChB,CAACA,cAAAA,aAAa,MAAM,UAAU,SAAS,GAEvC,OAAO;CAET,MAAM,QAAQ,mBAAmB,MAAM,eAAe,QAAQ,GAAG,CAAC,KAAK,IAAI;CAC3E,OACE,OAAO,SAAS,qBAAqBA,cAAAA,aAAa,MAAM,UAAU,QAAQ;AAE9E;AAGA,MAAM,6BAA6B,SAAS;CAC1C,IAAI,MAAM,SAAS,oBACjB,OAAO;CAET,IACE,MAAM,SAAS,oBACf,KAAK,gBAAgB,SAAS,qBAC9BA,cAAAA,aAAa,KAAK,eAAe,UAAU,OAAO,GAElD,OAAO,0BAA0B,KAAK,UAAU;CAElD,OAAO;AACT;AAEA,IAAA,uCAAA,GAAA,gBAAA,mBAAA,CAAkC;CAChC,MAAM,EAAE,MAAM,8BAA8B;CAC5C,OAAO,EACL,+BAA+B;EAC7B,MAAM;GACJ,MAAM;GACN,UAAU,EACR,0BACE,6VAMJ;EACF;EACA,WAAW,SAAS;GAClB,OAAO,EACL,sBAAsB,MAAM;IAC1B,IAAI,CAAC,oBAAoB,KAAK,cAAc,GAC1C;IAEF,IAAI,CAAC,0BAA0B,KAAK,UAAU,GAC5C;IAEF,QAAQ,OAAO;KAAE;KAAM,WAAW;IAA2B,CAAC;GAChE,EACF;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { r as isIdentifier } from "./utils.mjs";
|
|
2
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
//#region src/no-partial-record-satisfies.ts
|
|
4
|
+
const unwrapReadonlyType = (typeNode) => {
|
|
5
|
+
if (typeNode?.type === "TSTypeReference" && isIdentifier(typeNode.typeName, "Readonly")) return typeNode.typeArguments?.params?.at(0) ?? null;
|
|
6
|
+
return typeNode;
|
|
7
|
+
};
|
|
8
|
+
const isPartialRecordType = (typeNode) => {
|
|
9
|
+
const outer = unwrapReadonlyType(typeNode);
|
|
10
|
+
if (outer?.type !== "TSTypeReference" || !isIdentifier(outer.typeName, "Partial")) return false;
|
|
11
|
+
const inner = unwrapReadonlyType(outer.typeArguments?.params?.at(0) ?? null);
|
|
12
|
+
return inner?.type === "TSTypeReference" && isIdentifier(inner.typeName, "Record");
|
|
13
|
+
};
|
|
14
|
+
const isObjectLiteralExpression = (node) => {
|
|
15
|
+
if (node?.type === "ObjectExpression") return true;
|
|
16
|
+
if (node?.type === "TSAsExpression" && node.typeAnnotation?.type === "TSTypeReference" && isIdentifier(node.typeAnnotation.typeName, "const")) return isObjectLiteralExpression(node.expression);
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
var no_partial_record_satisfies_default = eslintCompatPlugin({
|
|
20
|
+
meta: { name: "no-partial-record-satisfies" },
|
|
21
|
+
rules: { "no-partial-record-satisfies": {
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
messages: { noPartialRecordSatisfies: "Don't pin an object literal with `satisfies Partial<Record<...>>` — Partial cancels the totality check satisfies would otherwise give you, so a union member can silently fall through. Make the record total (explicit 'none'/false/null per member), narrow the key union, or add an eslint-disable naming what absence means for genuinely sparse data." }
|
|
25
|
+
},
|
|
26
|
+
createOnce(context) {
|
|
27
|
+
return { TSSatisfiesExpression(node) {
|
|
28
|
+
if (!isPartialRecordType(node.typeAnnotation)) return;
|
|
29
|
+
if (!isObjectLiteralExpression(node.expression)) return;
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "noPartialRecordSatisfies"
|
|
33
|
+
});
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
36
|
+
} }
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { no_partial_record_satisfies_default as default };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=no-partial-record-satisfies.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-partial-record-satisfies.mjs","names":[],"sources":["../src/no-partial-record-satisfies.ts"],"sourcesContent":["import { eslintCompatPlugin } from \"@oxlint/plugins\";\n// Ban `<object literal> satisfies Partial<Record<Union, T>>` (with or\n// without a leading `as const`).\n//\n// A const object literal pinned with `satisfies` against a `Record` keyed by\n// a union declares itself the classification of every union member —\n// `satisfies` fails typecheck the moment a new member is added and left\n// unhandled. Wrapping that `Record` in `Partial` cancels the guarantee: a\n// missing member typechecks silently, so the literal can drift out of sync\n// with the union with no compiler signal. That silent-drift surface can cause\n// a classification-table bug when a newly added member falls through a\n// `Partial<Record<...>>` lookup instead of failing typecheck.\n//\n// `Readonly<...>` wrapping either side (`Readonly<Partial<Record<...>>>` or\n// `Partial<Readonly<Record<...>>>`) is unwrapped before the check, since it\n// changes mutability, not totality.\n//\n// Only the `satisfies` expression on an object literal is flagged. Plain\n// type-annotation positions (`const x: Partial<Record<...>> = {...}`),\n// function parameters, and variable/return type annotations are exempt:\n// those are legitimate uses (override inputs, accumulators, genuinely sparse\n// data) and carry no compiler guarantee of totality to lose.\n//\n// Flagged:\n// const x = {\n// a: 1,\n// } as const satisfies Partial<Record<Union, number>>;\n// const y = { a: 1 } satisfies Partial<Record<Union, number>>;\n// const z = {\n// a: 1,\n// } as const satisfies Readonly<Partial<Record<Union, number>>>;\n//\n// Allowed:\n// const x = {\n// a: 1,\n// } as const satisfies Record<Union, number>;\n// const fn = (overrides: Partial<Record<Union, number>>) => overrides;\n// let acc: Partial<Record<Union, number>> = {};\n// type Sparse = Partial<Record<Union, number>>;\n//\n// Fix by making the record total over the union — an explicit \"none\" /\n// `false` / `null` value for members where nothing applies forces a new\n// union member to be classified before it typechecks. If only a subset of\n// the union is ever relevant, derive a narrower union for the keys instead\n// of falling back to `Partial`. For genuinely sparse data where absence has\n// documented meaning (e.g. an unbounded key space), add an eslint-disable\n// with a comment naming what absence means.\n\nimport { isIdentifier } from \"./utils.ts\";\n\nconst unwrapReadonlyType = (typeNode) => {\n if (\n typeNode?.type === \"TSTypeReference\" &&\n isIdentifier(typeNode.typeName, \"Readonly\")\n ) {\n return typeNode.typeArguments?.params?.at(0) ?? null;\n }\n return typeNode;\n};\n\n// Matches `Partial<Record<...>>`, unwrapping a `Readonly<...>` wrapper on\n// either the outer (`Readonly<Partial<Record<...>>>`) or inner\n// (`Partial<Readonly<Record<...>>>`) type.\nconst isPartialRecordType = (typeNode) => {\n const outer = unwrapReadonlyType(typeNode);\n if (\n outer?.type !== \"TSTypeReference\" ||\n !isIdentifier(outer.typeName, \"Partial\")\n ) {\n return false;\n }\n const inner = unwrapReadonlyType(outer.typeArguments?.params?.at(0) ?? null);\n return (\n inner?.type === \"TSTypeReference\" && isIdentifier(inner.typeName, \"Record\")\n );\n};\n\n// Matches an object literal, or an object literal wrapped in `as const`.\nconst isObjectLiteralExpression = (node) => {\n if (node?.type === \"ObjectExpression\") {\n return true;\n }\n if (\n node?.type === \"TSAsExpression\" &&\n node.typeAnnotation?.type === \"TSTypeReference\" &&\n isIdentifier(node.typeAnnotation.typeName, \"const\")\n ) {\n return isObjectLiteralExpression(node.expression);\n }\n return false;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-partial-record-satisfies\" },\n rules: {\n \"no-partial-record-satisfies\": {\n meta: {\n type: \"problem\",\n messages: {\n noPartialRecordSatisfies:\n \"Don't pin an object literal with `satisfies Partial<Record<...>>` \" +\n \"— Partial cancels the totality check satisfies would otherwise \" +\n \"give you, so a union member can silently fall through. Make the \" +\n \"record total (explicit 'none'/false/null per member), narrow the \" +\n \"key union, or add an eslint-disable naming what absence means \" +\n \"for genuinely sparse data.\",\n },\n },\n createOnce(context) {\n return {\n TSSatisfiesExpression(node) {\n if (!isPartialRecordType(node.typeAnnotation)) {\n return;\n }\n if (!isObjectLiteralExpression(node.expression)) {\n return;\n }\n context.report({ node, messageId: \"noPartialRecordSatisfies\" });\n },\n };\n },\n },\n },\n});\n"],"mappings":";;;AAkDA,MAAM,sBAAsB,aAAa;CACvC,IACE,UAAU,SAAS,qBACnB,aAAa,SAAS,UAAU,UAAU,GAE1C,OAAO,SAAS,eAAe,QAAQ,GAAG,CAAC,KAAK;CAElD,OAAO;AACT;AAKA,MAAM,uBAAuB,aAAa;CACxC,MAAM,QAAQ,mBAAmB,QAAQ;CACzC,IACE,OAAO,SAAS,qBAChB,CAAC,aAAa,MAAM,UAAU,SAAS,GAEvC,OAAO;CAET,MAAM,QAAQ,mBAAmB,MAAM,eAAe,QAAQ,GAAG,CAAC,KAAK,IAAI;CAC3E,OACE,OAAO,SAAS,qBAAqB,aAAa,MAAM,UAAU,QAAQ;AAE9E;AAGA,MAAM,6BAA6B,SAAS;CAC1C,IAAI,MAAM,SAAS,oBACjB,OAAO;CAET,IACE,MAAM,SAAS,oBACf,KAAK,gBAAgB,SAAS,qBAC9B,aAAa,KAAK,eAAe,UAAU,OAAO,GAElD,OAAO,0BAA0B,KAAK,UAAU;CAElD,OAAO;AACT;AAEA,IAAA,sCAAe,mBAAmB;CAChC,MAAM,EAAE,MAAM,8BAA8B;CAC5C,OAAO,EACL,+BAA+B;EAC7B,MAAM;GACJ,MAAM;GACN,UAAU,EACR,0BACE,6VAMJ;EACF;EACA,WAAW,SAAS;GAClB,OAAO,EACL,sBAAsB,MAAM;IAC1B,IAAI,CAAC,oBAAoB,KAAK,cAAc,GAC1C;IAEF,IAAI,CAAC,0BAA0B,KAAK,UAAU,GAC5C;IAEF,QAAQ,OAAO;KAAE;KAAM,WAAW;IAA2B,CAAC;GAChE,EACF;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
const require_utils = require("./utils.cjs");
|
|
6
|
+
let _oxlint_plugins = require("@oxlint/plugins");
|
|
7
|
+
//#region src/no-unsafe-inner-html.ts
|
|
8
|
+
const HTML_SANITIZER_LEAF_RE = /^(?:escape(?:html|xml)|sanitize(?:html|dom)|purify(?:html|dom)|dompurify)$/iu;
|
|
9
|
+
const DOM_PURIFY_RE = /^dompurify$/iu;
|
|
10
|
+
const SANITIZER_RESULT_MEMBERS = /* @__PURE__ */ new Set([
|
|
11
|
+
"value",
|
|
12
|
+
"data",
|
|
13
|
+
"html"
|
|
14
|
+
]);
|
|
15
|
+
const ESCAPE_HATCH_RE = /^\s*safe-html:/u;
|
|
16
|
+
const isComment = (value) => typeof value === "object" && value !== null && typeof value.value === "string" && typeof value.loc === "object" && value.loc !== null;
|
|
17
|
+
const isJsxIdentifier = (node, name) => typeof node === "object" && node !== null && node.type === "JSXIdentifier" && node.name === name;
|
|
18
|
+
const isHtmlSanitizerCalleeName = (dotted) => {
|
|
19
|
+
const parts = dotted.split(".");
|
|
20
|
+
const leaf = parts.at(-1);
|
|
21
|
+
if (leaf === void 0) return false;
|
|
22
|
+
if (HTML_SANITIZER_LEAF_RE.test(leaf)) return true;
|
|
23
|
+
const objectName = parts.at(-2);
|
|
24
|
+
return leaf === "sanitize" && DOM_PURIFY_RE.test(objectName ?? "");
|
|
25
|
+
};
|
|
26
|
+
const isSanitizerCall = (node) => {
|
|
27
|
+
if (node.type !== "CallExpression") return false;
|
|
28
|
+
const dotted = require_utils.getCalleeName(node.callee);
|
|
29
|
+
return dotted !== null && isHtmlSanitizerCalleeName(dotted);
|
|
30
|
+
};
|
|
31
|
+
const isProvenSafeValue = (node) => {
|
|
32
|
+
if (!node || typeof node.type !== "string") return false;
|
|
33
|
+
if (node.type === "Literal" && typeof node.value === "string") return true;
|
|
34
|
+
if (node.type === "TemplateLiteral") return Array.isArray(node.expressions) && node.expressions.every(isProvenSafeValue);
|
|
35
|
+
if (isSanitizerCall(node)) return true;
|
|
36
|
+
if (node.type === "MemberExpression" && node.computed === false && isProvenSafeMemberLeaf(node)) return true;
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
const isProvenSafeMemberLeaf = (node) => {
|
|
40
|
+
const propertyName = require_utils.getPropertyName(node.property);
|
|
41
|
+
if (propertyName === null || !SANITIZER_RESULT_MEMBERS.has(propertyName)) return false;
|
|
42
|
+
return isSanitizerCall(node.object);
|
|
43
|
+
};
|
|
44
|
+
const isDangerouslySetInnerHtmlValue = (property) => {
|
|
45
|
+
if (require_utils.getPropertyName(property.key) !== "__html") return false;
|
|
46
|
+
const objectExpression = property.parent;
|
|
47
|
+
if (objectExpression?.type !== "ObjectExpression") return false;
|
|
48
|
+
let current = objectExpression.parent;
|
|
49
|
+
while (current?.type === "TSAsExpression" || current?.type === "TSSatisfiesExpression") current = current.parent;
|
|
50
|
+
if (current?.type !== "JSXExpressionContainer") return false;
|
|
51
|
+
const attribute = current.parent;
|
|
52
|
+
return attribute?.type === "JSXAttribute" && isJsxIdentifier(attribute.name, "dangerouslySetInnerHTML");
|
|
53
|
+
};
|
|
54
|
+
var no_unsafe_inner_html_default = (0, _oxlint_plugins.eslintCompatPlugin)({
|
|
55
|
+
meta: { name: "no-unsafe-inner-html" },
|
|
56
|
+
rules: { "no-unsafe-inner-html": {
|
|
57
|
+
meta: {
|
|
58
|
+
type: "problem",
|
|
59
|
+
messages: {
|
|
60
|
+
unsafeInnerHtml: "Raw HTML injected into the DOM must come from a provably sanitized/escaped value: a static string, an HTML/DOM sanitize/escape call (or its .value/.data/.html result). Wrap the value in a sanitizer, or add a `// safe-html: <reason naming the sanitizer/source>` comment on the line directly above if it is escaped at its source.",
|
|
61
|
+
unsafeInnerHtmlSpread: "Do not spread an object into dangerouslySetInnerHTML. Keep `__html` inline so this rule can prove the HTML value is sanitized or escaped."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
createOnce(context) {
|
|
65
|
+
const escapeHatchLines = /* @__PURE__ */ new Set();
|
|
66
|
+
const recordEscapeHatches = (node) => {
|
|
67
|
+
const comments = node && Array.isArray(node.comments) ? node.comments.filter(isComment) : [];
|
|
68
|
+
for (const comment of comments) if (ESCAPE_HATCH_RE.test(comment.value)) escapeHatchLines.add(comment.loc.end.line);
|
|
69
|
+
};
|
|
70
|
+
const hasEscapeHatchAbove = (node) => escapeHatchLines.has(node.loc.start.line - 1);
|
|
71
|
+
const reportIfUnsafe = (node) => {
|
|
72
|
+
if (isProvenSafeValue(node)) return;
|
|
73
|
+
if (hasEscapeHatchAbove(node)) return;
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: "unsafeInnerHtml"
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const reportPayloadSpreads = (objectNode) => {
|
|
80
|
+
for (const property of objectNode.properties) {
|
|
81
|
+
if (property?.type !== "SpreadElement") continue;
|
|
82
|
+
context.report({
|
|
83
|
+
node: property,
|
|
84
|
+
messageId: "unsafeInnerHtmlSpread"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
before() {
|
|
90
|
+
escapeHatchLines.clear();
|
|
91
|
+
},
|
|
92
|
+
Program(node) {
|
|
93
|
+
recordEscapeHatches(node);
|
|
94
|
+
},
|
|
95
|
+
JSXAttribute(node) {
|
|
96
|
+
if (!isJsxIdentifier(node.name, "dangerouslySetInnerHTML")) return;
|
|
97
|
+
const value = node.value;
|
|
98
|
+
if (value?.type !== "JSXExpressionContainer") return;
|
|
99
|
+
if (!value.expression) return;
|
|
100
|
+
const expression = require_utils.unwrapExpression(value.expression);
|
|
101
|
+
if (expression?.type === "ObjectExpression") {
|
|
102
|
+
reportPayloadSpreads(expression);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
reportIfUnsafe(value.expression);
|
|
106
|
+
},
|
|
107
|
+
Property(node) {
|
|
108
|
+
if (!isDangerouslySetInnerHtmlValue(node)) return;
|
|
109
|
+
reportIfUnsafe(node.value);
|
|
110
|
+
},
|
|
111
|
+
AssignmentExpression(node) {
|
|
112
|
+
const target = node.left;
|
|
113
|
+
if (target.type !== "MemberExpression" || target.computed !== false || !require_utils.isIdentifier(target.property, "innerHTML")) return;
|
|
114
|
+
reportIfUnsafe(node.right);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} }
|
|
119
|
+
});
|
|
120
|
+
//#endregion
|
|
121
|
+
exports.default = no_unsafe_inner_html_default;
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=no-unsafe-inner-html.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unsafe-inner-html.cjs","names":["getCalleeName","getPropertyName","unwrapExpression","isIdentifier"],"sources":["../src/no-unsafe-inner-html.ts"],"sourcesContent":["import { eslintCompatPlugin } from \"@oxlint/plugins\";\n// Forbid injecting un-proven HTML into the DOM.\n//\n// Raw HTML may only reach the DOM from a value that is provably sanitized or\n// escaped. This precise guard can replace a blanket `react/no-danger` rule:\n// approved sanitizer calls remain usable while unescaped stored or user input\n// cannot flow into `__html` or `el.innerHTML` unnoticed.\n//\n// Two sinks share one allowlist:\n// • JSX `__html` property of a `dangerouslySetInnerHTML` object literal\n// (report on the value expression).\n// • `AssignmentExpression` whose LHS is a non-computed `.innerHTML`\n// MemberExpression (report on the RHS).\n//\n// A value is allowed when it is:\n// • a string Literal or TemplateLiteral whose interpolations are all\n// independently proven safe (static markup, no raw injection), OR\n// • a CallExpression whose callee is an HTML/DOM sanitizer or escaper\n// (`escapeHtml`, `escapeXml`, `sanitizeHtml`, `sanitizeDom`,\n// `DOMPurify.sanitize`), including a `.value` / `.data` / `.html`-style\n// member read OFF such a call (some sanitizers return `{ value }`), OR\n// • carries an explicit `// safe-html:` escape-hatch comment on the line\n// directly above the sink (loc adjacency, like suppression-hygiene.ts).\n//\n// A value is NOT auto-allowed just because the identifier is named\n// `html` / `content` / `headline` / `body` — those are the sinks, not\n// proof of safety. Prove safety at the source or annotate with a reason.\n//\n// Flagged:\n// <div dangerouslySetInnerHTML={{ __html: userInput }} />\n// <div dangerouslySetInnerHTML={{ __html: hit.headline }} /> // unless annotated\n// el.innerHTML = html; // unless annotated\n// el.innerHTML = data.body;\n//\n// Allowed:\n// <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(x) }} />\n// <div dangerouslySetInnerHTML={{ __html: `<b>${escapeXml(n)}</b>` }} />\n// <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(x) }} />\n// el.innerHTML = \"\";\n// el.innerHTML = \" \";\n// // safe-html: escaped by escapeAndHighlight()\n// <div dangerouslySetInnerHTML={{ __html: hit.headline }} />\n\nimport {\n getCalleeName,\n getPropertyName,\n isIdentifier,\n unwrapExpression,\n} from \"./utils.ts\";\n\n// Callee names that prove the value was run through an HTML/DOM sanitizer or\n// escaper. Keep this narrow: `escapeRegex()` and `sanitizeFilename()` are not\n// HTML-safe.\nconst HTML_SANITIZER_LEAF_RE =\n /^(?:escape(?:html|xml)|sanitize(?:html|dom)|purify(?:html|dom)|dompurify)$/iu;\n\nconst DOM_PURIFY_RE = /^dompurify$/iu;\n\n// Members that some sanitizers expose on their result object\n// (`sanitizeHtml(x).value`, `sanitizeHtml(x).html`). Reading one of these OFF a\n// sanitizer call is still proven-safe.\nconst SANITIZER_RESULT_MEMBERS = new Set([\"value\", \"data\", \"html\"]);\n\nconst ESCAPE_HATCH_RE = /^\\s*safe-html:/u;\n\nconst isComment = (value) =>\n typeof value === \"object\" &&\n value !== null &&\n typeof value.value === \"string\" &&\n typeof value.loc === \"object\" &&\n value.loc !== null;\n\nconst isJsxIdentifier = (node, name) =>\n typeof node === \"object\" &&\n node !== null &&\n node.type === \"JSXIdentifier\" &&\n node.name === name;\n\nconst isHtmlSanitizerCalleeName = (dotted) => {\n const parts = dotted.split(\".\");\n const leaf = parts.at(-1);\n if (leaf === undefined) {\n return false;\n }\n if (HTML_SANITIZER_LEAF_RE.test(leaf)) {\n return true;\n }\n const objectName = parts.at(-2);\n return leaf === \"sanitize\" && DOM_PURIFY_RE.test(objectName ?? \"\");\n};\n\nconst isSanitizerCall = (node) => {\n if (node.type !== \"CallExpression\") {\n return false;\n }\n const dotted = getCalleeName(node.callee);\n return dotted !== null && isHtmlSanitizerCalleeName(dotted);\n};\n\n// A value is proven safe by its own shape (independent of any comment):\n// static string, or the result of a sanitizer call (directly, or via a\n// `.value`/`.data`/`.html` accessor off the call).\nconst isProvenSafeValue = (node) => {\n if (!node || typeof node.type !== \"string\") {\n return false;\n }\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n return true;\n }\n if (node.type === \"TemplateLiteral\") {\n return (\n Array.isArray(node.expressions) &&\n node.expressions.every(isProvenSafeValue)\n );\n }\n if (isSanitizerCall(node)) {\n return true;\n }\n // `sanitize(x).value`, `purify(x).html` — a result accessor off a call.\n if (\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isProvenSafeMemberLeaf(node)\n ) {\n return true;\n }\n return false;\n};\n\nconst isProvenSafeMemberLeaf = (node) => {\n const propertyName = getPropertyName(node.property);\n if (propertyName === null || !SANITIZER_RESULT_MEMBERS.has(propertyName)) {\n return false;\n }\n return isSanitizerCall(node.object);\n};\n\nconst isDangerouslySetInnerHtmlValue = (property) => {\n if (getPropertyName(property.key) !== \"__html\") {\n return false;\n }\n const objectExpression = property.parent;\n if (objectExpression?.type !== \"ObjectExpression\") {\n return false;\n }\n\n let current = objectExpression.parent;\n while (\n current?.type === \"TSAsExpression\" ||\n current?.type === \"TSSatisfiesExpression\"\n ) {\n current = current.parent;\n }\n\n if (current?.type !== \"JSXExpressionContainer\") {\n return false;\n }\n const attribute = current.parent;\n return (\n attribute?.type === \"JSXAttribute\" &&\n isJsxIdentifier(attribute.name, \"dangerouslySetInnerHTML\")\n );\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-unsafe-inner-html\" },\n rules: {\n \"no-unsafe-inner-html\": {\n meta: {\n type: \"problem\",\n messages: {\n unsafeInnerHtml:\n \"Raw HTML injected into the DOM must come from a provably \" +\n \"sanitized/escaped value: a static string, an HTML/DOM \" +\n \"sanitize/escape call (or its .value/.data/.html result). \" +\n \"Wrap the value in a sanitizer, or add a `// safe-html: <reason \" +\n \"naming the sanitizer/source>` comment on the line directly above \" +\n \"if it is escaped at its source.\",\n unsafeInnerHtmlSpread:\n \"Do not spread an object into dangerouslySetInnerHTML. Keep \" +\n \"`__html` inline so this rule can prove the HTML value is \" +\n \"sanitized or escaped.\",\n },\n },\n createOnce(context) {\n const escapeHatchLines = new Set();\n\n const recordEscapeHatches = (node) => {\n const comments =\n node && Array.isArray(node.comments)\n ? node.comments.filter(isComment)\n : [];\n for (const comment of comments) {\n if (ESCAPE_HATCH_RE.test(comment.value)) {\n escapeHatchLines.add(comment.loc.end.line);\n }\n }\n };\n\n // The escape-hatch comment sits on the line directly above the\n // reported node's first line. Sink expressions inside JSX object\n // literals span multiple lines, so anchor on the node's start line.\n const hasEscapeHatchAbove = (node) =>\n escapeHatchLines.has(node.loc.start.line - 1);\n\n const reportIfUnsafe = (node) => {\n if (isProvenSafeValue(node)) {\n return;\n }\n if (hasEscapeHatchAbove(node)) {\n return;\n }\n context.report({ node, messageId: \"unsafeInnerHtml\" });\n };\n\n const reportPayloadSpreads = (objectNode) => {\n for (const property of objectNode.properties) {\n if (property?.type !== \"SpreadElement\") {\n continue;\n }\n context.report({\n node: property,\n messageId: \"unsafeInnerHtmlSpread\",\n });\n }\n };\n\n return {\n before() {\n escapeHatchLines.clear();\n },\n Program(node) {\n recordEscapeHatches(node);\n },\n\n // Reject hoisted payloads such as\n // `dangerouslySetInnerHTML={payload}`. Keeping the `__html` object\n // inline lets this rule inspect the actual HTML expression.\n JSXAttribute(node) {\n if (!isJsxIdentifier(node.name, \"dangerouslySetInnerHTML\")) {\n return;\n }\n const value = node.value;\n if (value?.type !== \"JSXExpressionContainer\") {\n return;\n }\n if (!value.expression) {\n return;\n }\n const expression = unwrapExpression(value.expression);\n if (expression?.type === \"ObjectExpression\") {\n reportPayloadSpreads(expression);\n return;\n }\n reportIfUnsafe(value.expression);\n },\n\n // Sink 1: `dangerouslySetInnerHTML={{ __html: <expr> }}`.\n Property(node) {\n if (!isDangerouslySetInnerHtmlValue(node)) {\n return;\n }\n reportIfUnsafe(node.value);\n },\n\n // Sink 2: `<el>.innerHTML = <expr>` (non-computed member LHS).\n AssignmentExpression(node) {\n const target = node.left;\n if (\n target.type !== \"MemberExpression\" ||\n target.computed !== false ||\n !isIdentifier(target.property, \"innerHTML\")\n ) {\n return;\n }\n reportIfUnsafe(node.right);\n },\n };\n },\n },\n },\n});\n"],"mappings":";;;;;;;AAqDA,MAAM,yBACJ;AAEF,MAAM,gBAAgB;AAKtB,MAAM,2CAA2B,IAAI,IAAI;CAAC;CAAS;CAAQ;AAAM,CAAC;AAElE,MAAM,kBAAkB;AAExB,MAAM,aAAa,UACjB,OAAO,UAAU,YACjB,UAAU,QACV,OAAO,MAAM,UAAU,YACvB,OAAO,MAAM,QAAQ,YACrB,MAAM,QAAQ;AAEhB,MAAM,mBAAmB,MAAM,SAC7B,OAAO,SAAS,YAChB,SAAS,QACT,KAAK,SAAS,mBACd,KAAK,SAAS;AAEhB,MAAM,6BAA6B,WAAW;CAC5C,MAAM,QAAQ,OAAO,MAAM,GAAG;CAC9B,MAAM,OAAO,MAAM,GAAG,EAAE;CACxB,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI,uBAAuB,KAAK,IAAI,GAClC,OAAO;CAET,MAAM,aAAa,MAAM,GAAG,EAAE;CAC9B,OAAO,SAAS,cAAc,cAAc,KAAK,cAAc,EAAE;AACnE;AAEA,MAAM,mBAAmB,SAAS;CAChC,IAAI,KAAK,SAAS,kBAChB,OAAO;CAET,MAAM,SAASA,cAAAA,cAAc,KAAK,MAAM;CACxC,OAAO,WAAW,QAAQ,0BAA0B,MAAM;AAC5D;AAKA,MAAM,qBAAqB,SAAS;CAClC,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,UAChC,OAAO;CAET,IAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UACnD,OAAO;CAET,IAAI,KAAK,SAAS,mBAChB,OACE,MAAM,QAAQ,KAAK,WAAW,KAC9B,KAAK,YAAY,MAAM,iBAAiB;CAG5C,IAAI,gBAAgB,IAAI,GACtB,OAAO;CAGT,IACE,KAAK,SAAS,sBACd,KAAK,aAAa,SAClB,uBAAuB,IAAI,GAE3B,OAAO;CAET,OAAO;AACT;AAEA,MAAM,0BAA0B,SAAS;CACvC,MAAM,eAAeC,cAAAA,gBAAgB,KAAK,QAAQ;CAClD,IAAI,iBAAiB,QAAQ,CAAC,yBAAyB,IAAI,YAAY,GACrE,OAAO;CAET,OAAO,gBAAgB,KAAK,MAAM;AACpC;AAEA,MAAM,kCAAkC,aAAa;CACnD,IAAIA,cAAAA,gBAAgB,SAAS,GAAG,MAAM,UACpC,OAAO;CAET,MAAM,mBAAmB,SAAS;CAClC,IAAI,kBAAkB,SAAS,oBAC7B,OAAO;CAGT,IAAI,UAAU,iBAAiB;CAC/B,OACE,SAAS,SAAS,oBAClB,SAAS,SAAS,yBAElB,UAAU,QAAQ;CAGpB,IAAI,SAAS,SAAS,0BACpB,OAAO;CAET,MAAM,YAAY,QAAQ;CAC1B,OACE,WAAW,SAAS,kBACpB,gBAAgB,UAAU,MAAM,yBAAyB;AAE7D;AAEA,IAAA,gCAAA,GAAA,gBAAA,mBAAA,CAAkC;CAChC,MAAM,EAAE,MAAM,uBAAuB;CACrC,OAAO,EACL,wBAAwB;EACtB,MAAM;GACJ,MAAM;GACN,UAAU;IACR,iBACE;IAMF,uBACE;GAGJ;EACF;EACA,WAAW,SAAS;GAClB,MAAM,mCAAmB,IAAI,IAAI;GAEjC,MAAM,uBAAuB,SAAS;IACpC,MAAM,WACJ,QAAQ,MAAM,QAAQ,KAAK,QAAQ,IAC/B,KAAK,SAAS,OAAO,SAAS,IAC9B,CAAC;IACP,KAAK,MAAM,WAAW,UACpB,IAAI,gBAAgB,KAAK,QAAQ,KAAK,GACpC,iBAAiB,IAAI,QAAQ,IAAI,IAAI,IAAI;GAG/C;GAKA,MAAM,uBAAuB,SAC3B,iBAAiB,IAAI,KAAK,IAAI,MAAM,OAAO,CAAC;GAE9C,MAAM,kBAAkB,SAAS;IAC/B,IAAI,kBAAkB,IAAI,GACxB;IAEF,IAAI,oBAAoB,IAAI,GAC1B;IAEF,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAkB,CAAC;GACvD;GAEA,MAAM,wBAAwB,eAAe;IAC3C,KAAK,MAAM,YAAY,WAAW,YAAY;KAC5C,IAAI,UAAU,SAAS,iBACrB;KAEF,QAAQ,OAAO;MACb,MAAM;MACN,WAAW;KACb,CAAC;IACH;GACF;GAEA,OAAO;IACL,SAAS;KACP,iBAAiB,MAAM;IACzB;IACA,QAAQ,MAAM;KACZ,oBAAoB,IAAI;IAC1B;IAKA,aAAa,MAAM;KACjB,IAAI,CAAC,gBAAgB,KAAK,MAAM,yBAAyB,GACvD;KAEF,MAAM,QAAQ,KAAK;KACnB,IAAI,OAAO,SAAS,0BAClB;KAEF,IAAI,CAAC,MAAM,YACT;KAEF,MAAM,aAAaC,cAAAA,iBAAiB,MAAM,UAAU;KACpD,IAAI,YAAY,SAAS,oBAAoB;MAC3C,qBAAqB,UAAU;MAC/B;KACF;KACA,eAAe,MAAM,UAAU;IACjC;IAGA,SAAS,MAAM;KACb,IAAI,CAAC,+BAA+B,IAAI,GACtC;KAEF,eAAe,KAAK,KAAK;IAC3B;IAGA,qBAAqB,MAAM;KACzB,MAAM,SAAS,KAAK;KACpB,IACE,OAAO,SAAS,sBAChB,OAAO,aAAa,SACpB,CAACC,cAAAA,aAAa,OAAO,UAAU,WAAW,GAE1C;KAEF,eAAe,KAAK,KAAK;IAC3B;GACF;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { i as unwrapExpression, n as getPropertyName, r as isIdentifier, t as getCalleeName } from "./utils.mjs";
|
|
2
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
//#region src/no-unsafe-inner-html.ts
|
|
4
|
+
const HTML_SANITIZER_LEAF_RE = /^(?:escape(?:html|xml)|sanitize(?:html|dom)|purify(?:html|dom)|dompurify)$/iu;
|
|
5
|
+
const DOM_PURIFY_RE = /^dompurify$/iu;
|
|
6
|
+
const SANITIZER_RESULT_MEMBERS = /* @__PURE__ */ new Set([
|
|
7
|
+
"value",
|
|
8
|
+
"data",
|
|
9
|
+
"html"
|
|
10
|
+
]);
|
|
11
|
+
const ESCAPE_HATCH_RE = /^\s*safe-html:/u;
|
|
12
|
+
const isComment = (value) => typeof value === "object" && value !== null && typeof value.value === "string" && typeof value.loc === "object" && value.loc !== null;
|
|
13
|
+
const isJsxIdentifier = (node, name) => typeof node === "object" && node !== null && node.type === "JSXIdentifier" && node.name === name;
|
|
14
|
+
const isHtmlSanitizerCalleeName = (dotted) => {
|
|
15
|
+
const parts = dotted.split(".");
|
|
16
|
+
const leaf = parts.at(-1);
|
|
17
|
+
if (leaf === void 0) return false;
|
|
18
|
+
if (HTML_SANITIZER_LEAF_RE.test(leaf)) return true;
|
|
19
|
+
const objectName = parts.at(-2);
|
|
20
|
+
return leaf === "sanitize" && DOM_PURIFY_RE.test(objectName ?? "");
|
|
21
|
+
};
|
|
22
|
+
const isSanitizerCall = (node) => {
|
|
23
|
+
if (node.type !== "CallExpression") return false;
|
|
24
|
+
const dotted = getCalleeName(node.callee);
|
|
25
|
+
return dotted !== null && isHtmlSanitizerCalleeName(dotted);
|
|
26
|
+
};
|
|
27
|
+
const isProvenSafeValue = (node) => {
|
|
28
|
+
if (!node || typeof node.type !== "string") return false;
|
|
29
|
+
if (node.type === "Literal" && typeof node.value === "string") return true;
|
|
30
|
+
if (node.type === "TemplateLiteral") return Array.isArray(node.expressions) && node.expressions.every(isProvenSafeValue);
|
|
31
|
+
if (isSanitizerCall(node)) return true;
|
|
32
|
+
if (node.type === "MemberExpression" && node.computed === false && isProvenSafeMemberLeaf(node)) return true;
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
const isProvenSafeMemberLeaf = (node) => {
|
|
36
|
+
const propertyName = getPropertyName(node.property);
|
|
37
|
+
if (propertyName === null || !SANITIZER_RESULT_MEMBERS.has(propertyName)) return false;
|
|
38
|
+
return isSanitizerCall(node.object);
|
|
39
|
+
};
|
|
40
|
+
const isDangerouslySetInnerHtmlValue = (property) => {
|
|
41
|
+
if (getPropertyName(property.key) !== "__html") return false;
|
|
42
|
+
const objectExpression = property.parent;
|
|
43
|
+
if (objectExpression?.type !== "ObjectExpression") return false;
|
|
44
|
+
let current = objectExpression.parent;
|
|
45
|
+
while (current?.type === "TSAsExpression" || current?.type === "TSSatisfiesExpression") current = current.parent;
|
|
46
|
+
if (current?.type !== "JSXExpressionContainer") return false;
|
|
47
|
+
const attribute = current.parent;
|
|
48
|
+
return attribute?.type === "JSXAttribute" && isJsxIdentifier(attribute.name, "dangerouslySetInnerHTML");
|
|
49
|
+
};
|
|
50
|
+
var no_unsafe_inner_html_default = eslintCompatPlugin({
|
|
51
|
+
meta: { name: "no-unsafe-inner-html" },
|
|
52
|
+
rules: { "no-unsafe-inner-html": {
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
messages: {
|
|
56
|
+
unsafeInnerHtml: "Raw HTML injected into the DOM must come from a provably sanitized/escaped value: a static string, an HTML/DOM sanitize/escape call (or its .value/.data/.html result). Wrap the value in a sanitizer, or add a `// safe-html: <reason naming the sanitizer/source>` comment on the line directly above if it is escaped at its source.",
|
|
57
|
+
unsafeInnerHtmlSpread: "Do not spread an object into dangerouslySetInnerHTML. Keep `__html` inline so this rule can prove the HTML value is sanitized or escaped."
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
createOnce(context) {
|
|
61
|
+
const escapeHatchLines = /* @__PURE__ */ new Set();
|
|
62
|
+
const recordEscapeHatches = (node) => {
|
|
63
|
+
const comments = node && Array.isArray(node.comments) ? node.comments.filter(isComment) : [];
|
|
64
|
+
for (const comment of comments) if (ESCAPE_HATCH_RE.test(comment.value)) escapeHatchLines.add(comment.loc.end.line);
|
|
65
|
+
};
|
|
66
|
+
const hasEscapeHatchAbove = (node) => escapeHatchLines.has(node.loc.start.line - 1);
|
|
67
|
+
const reportIfUnsafe = (node) => {
|
|
68
|
+
if (isProvenSafeValue(node)) return;
|
|
69
|
+
if (hasEscapeHatchAbove(node)) return;
|
|
70
|
+
context.report({
|
|
71
|
+
node,
|
|
72
|
+
messageId: "unsafeInnerHtml"
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const reportPayloadSpreads = (objectNode) => {
|
|
76
|
+
for (const property of objectNode.properties) {
|
|
77
|
+
if (property?.type !== "SpreadElement") continue;
|
|
78
|
+
context.report({
|
|
79
|
+
node: property,
|
|
80
|
+
messageId: "unsafeInnerHtmlSpread"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
before() {
|
|
86
|
+
escapeHatchLines.clear();
|
|
87
|
+
},
|
|
88
|
+
Program(node) {
|
|
89
|
+
recordEscapeHatches(node);
|
|
90
|
+
},
|
|
91
|
+
JSXAttribute(node) {
|
|
92
|
+
if (!isJsxIdentifier(node.name, "dangerouslySetInnerHTML")) return;
|
|
93
|
+
const value = node.value;
|
|
94
|
+
if (value?.type !== "JSXExpressionContainer") return;
|
|
95
|
+
if (!value.expression) return;
|
|
96
|
+
const expression = unwrapExpression(value.expression);
|
|
97
|
+
if (expression?.type === "ObjectExpression") {
|
|
98
|
+
reportPayloadSpreads(expression);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
reportIfUnsafe(value.expression);
|
|
102
|
+
},
|
|
103
|
+
Property(node) {
|
|
104
|
+
if (!isDangerouslySetInnerHtmlValue(node)) return;
|
|
105
|
+
reportIfUnsafe(node.value);
|
|
106
|
+
},
|
|
107
|
+
AssignmentExpression(node) {
|
|
108
|
+
const target = node.left;
|
|
109
|
+
if (target.type !== "MemberExpression" || target.computed !== false || !isIdentifier(target.property, "innerHTML")) return;
|
|
110
|
+
reportIfUnsafe(node.right);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
} }
|
|
115
|
+
});
|
|
116
|
+
//#endregion
|
|
117
|
+
export { no_unsafe_inner_html_default as default };
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=no-unsafe-inner-html.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unsafe-inner-html.mjs","names":[],"sources":["../src/no-unsafe-inner-html.ts"],"sourcesContent":["import { eslintCompatPlugin } from \"@oxlint/plugins\";\n// Forbid injecting un-proven HTML into the DOM.\n//\n// Raw HTML may only reach the DOM from a value that is provably sanitized or\n// escaped. This precise guard can replace a blanket `react/no-danger` rule:\n// approved sanitizer calls remain usable while unescaped stored or user input\n// cannot flow into `__html` or `el.innerHTML` unnoticed.\n//\n// Two sinks share one allowlist:\n// • JSX `__html` property of a `dangerouslySetInnerHTML` object literal\n// (report on the value expression).\n// • `AssignmentExpression` whose LHS is a non-computed `.innerHTML`\n// MemberExpression (report on the RHS).\n//\n// A value is allowed when it is:\n// • a string Literal or TemplateLiteral whose interpolations are all\n// independently proven safe (static markup, no raw injection), OR\n// • a CallExpression whose callee is an HTML/DOM sanitizer or escaper\n// (`escapeHtml`, `escapeXml`, `sanitizeHtml`, `sanitizeDom`,\n// `DOMPurify.sanitize`), including a `.value` / `.data` / `.html`-style\n// member read OFF such a call (some sanitizers return `{ value }`), OR\n// • carries an explicit `// safe-html:` escape-hatch comment on the line\n// directly above the sink (loc adjacency, like suppression-hygiene.ts).\n//\n// A value is NOT auto-allowed just because the identifier is named\n// `html` / `content` / `headline` / `body` — those are the sinks, not\n// proof of safety. Prove safety at the source or annotate with a reason.\n//\n// Flagged:\n// <div dangerouslySetInnerHTML={{ __html: userInput }} />\n// <div dangerouslySetInnerHTML={{ __html: hit.headline }} /> // unless annotated\n// el.innerHTML = html; // unless annotated\n// el.innerHTML = data.body;\n//\n// Allowed:\n// <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(x) }} />\n// <div dangerouslySetInnerHTML={{ __html: `<b>${escapeXml(n)}</b>` }} />\n// <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(x) }} />\n// el.innerHTML = \"\";\n// el.innerHTML = \" \";\n// // safe-html: escaped by escapeAndHighlight()\n// <div dangerouslySetInnerHTML={{ __html: hit.headline }} />\n\nimport {\n getCalleeName,\n getPropertyName,\n isIdentifier,\n unwrapExpression,\n} from \"./utils.ts\";\n\n// Callee names that prove the value was run through an HTML/DOM sanitizer or\n// escaper. Keep this narrow: `escapeRegex()` and `sanitizeFilename()` are not\n// HTML-safe.\nconst HTML_SANITIZER_LEAF_RE =\n /^(?:escape(?:html|xml)|sanitize(?:html|dom)|purify(?:html|dom)|dompurify)$/iu;\n\nconst DOM_PURIFY_RE = /^dompurify$/iu;\n\n// Members that some sanitizers expose on their result object\n// (`sanitizeHtml(x).value`, `sanitizeHtml(x).html`). Reading one of these OFF a\n// sanitizer call is still proven-safe.\nconst SANITIZER_RESULT_MEMBERS = new Set([\"value\", \"data\", \"html\"]);\n\nconst ESCAPE_HATCH_RE = /^\\s*safe-html:/u;\n\nconst isComment = (value) =>\n typeof value === \"object\" &&\n value !== null &&\n typeof value.value === \"string\" &&\n typeof value.loc === \"object\" &&\n value.loc !== null;\n\nconst isJsxIdentifier = (node, name) =>\n typeof node === \"object\" &&\n node !== null &&\n node.type === \"JSXIdentifier\" &&\n node.name === name;\n\nconst isHtmlSanitizerCalleeName = (dotted) => {\n const parts = dotted.split(\".\");\n const leaf = parts.at(-1);\n if (leaf === undefined) {\n return false;\n }\n if (HTML_SANITIZER_LEAF_RE.test(leaf)) {\n return true;\n }\n const objectName = parts.at(-2);\n return leaf === \"sanitize\" && DOM_PURIFY_RE.test(objectName ?? \"\");\n};\n\nconst isSanitizerCall = (node) => {\n if (node.type !== \"CallExpression\") {\n return false;\n }\n const dotted = getCalleeName(node.callee);\n return dotted !== null && isHtmlSanitizerCalleeName(dotted);\n};\n\n// A value is proven safe by its own shape (independent of any comment):\n// static string, or the result of a sanitizer call (directly, or via a\n// `.value`/`.data`/`.html` accessor off the call).\nconst isProvenSafeValue = (node) => {\n if (!node || typeof node.type !== \"string\") {\n return false;\n }\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n return true;\n }\n if (node.type === \"TemplateLiteral\") {\n return (\n Array.isArray(node.expressions) &&\n node.expressions.every(isProvenSafeValue)\n );\n }\n if (isSanitizerCall(node)) {\n return true;\n }\n // `sanitize(x).value`, `purify(x).html` — a result accessor off a call.\n if (\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isProvenSafeMemberLeaf(node)\n ) {\n return true;\n }\n return false;\n};\n\nconst isProvenSafeMemberLeaf = (node) => {\n const propertyName = getPropertyName(node.property);\n if (propertyName === null || !SANITIZER_RESULT_MEMBERS.has(propertyName)) {\n return false;\n }\n return isSanitizerCall(node.object);\n};\n\nconst isDangerouslySetInnerHtmlValue = (property) => {\n if (getPropertyName(property.key) !== \"__html\") {\n return false;\n }\n const objectExpression = property.parent;\n if (objectExpression?.type !== \"ObjectExpression\") {\n return false;\n }\n\n let current = objectExpression.parent;\n while (\n current?.type === \"TSAsExpression\" ||\n current?.type === \"TSSatisfiesExpression\"\n ) {\n current = current.parent;\n }\n\n if (current?.type !== \"JSXExpressionContainer\") {\n return false;\n }\n const attribute = current.parent;\n return (\n attribute?.type === \"JSXAttribute\" &&\n isJsxIdentifier(attribute.name, \"dangerouslySetInnerHTML\")\n );\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"no-unsafe-inner-html\" },\n rules: {\n \"no-unsafe-inner-html\": {\n meta: {\n type: \"problem\",\n messages: {\n unsafeInnerHtml:\n \"Raw HTML injected into the DOM must come from a provably \" +\n \"sanitized/escaped value: a static string, an HTML/DOM \" +\n \"sanitize/escape call (or its .value/.data/.html result). \" +\n \"Wrap the value in a sanitizer, or add a `// safe-html: <reason \" +\n \"naming the sanitizer/source>` comment on the line directly above \" +\n \"if it is escaped at its source.\",\n unsafeInnerHtmlSpread:\n \"Do not spread an object into dangerouslySetInnerHTML. Keep \" +\n \"`__html` inline so this rule can prove the HTML value is \" +\n \"sanitized or escaped.\",\n },\n },\n createOnce(context) {\n const escapeHatchLines = new Set();\n\n const recordEscapeHatches = (node) => {\n const comments =\n node && Array.isArray(node.comments)\n ? node.comments.filter(isComment)\n : [];\n for (const comment of comments) {\n if (ESCAPE_HATCH_RE.test(comment.value)) {\n escapeHatchLines.add(comment.loc.end.line);\n }\n }\n };\n\n // The escape-hatch comment sits on the line directly above the\n // reported node's first line. Sink expressions inside JSX object\n // literals span multiple lines, so anchor on the node's start line.\n const hasEscapeHatchAbove = (node) =>\n escapeHatchLines.has(node.loc.start.line - 1);\n\n const reportIfUnsafe = (node) => {\n if (isProvenSafeValue(node)) {\n return;\n }\n if (hasEscapeHatchAbove(node)) {\n return;\n }\n context.report({ node, messageId: \"unsafeInnerHtml\" });\n };\n\n const reportPayloadSpreads = (objectNode) => {\n for (const property of objectNode.properties) {\n if (property?.type !== \"SpreadElement\") {\n continue;\n }\n context.report({\n node: property,\n messageId: \"unsafeInnerHtmlSpread\",\n });\n }\n };\n\n return {\n before() {\n escapeHatchLines.clear();\n },\n Program(node) {\n recordEscapeHatches(node);\n },\n\n // Reject hoisted payloads such as\n // `dangerouslySetInnerHTML={payload}`. Keeping the `__html` object\n // inline lets this rule inspect the actual HTML expression.\n JSXAttribute(node) {\n if (!isJsxIdentifier(node.name, \"dangerouslySetInnerHTML\")) {\n return;\n }\n const value = node.value;\n if (value?.type !== \"JSXExpressionContainer\") {\n return;\n }\n if (!value.expression) {\n return;\n }\n const expression = unwrapExpression(value.expression);\n if (expression?.type === \"ObjectExpression\") {\n reportPayloadSpreads(expression);\n return;\n }\n reportIfUnsafe(value.expression);\n },\n\n // Sink 1: `dangerouslySetInnerHTML={{ __html: <expr> }}`.\n Property(node) {\n if (!isDangerouslySetInnerHtmlValue(node)) {\n return;\n }\n reportIfUnsafe(node.value);\n },\n\n // Sink 2: `<el>.innerHTML = <expr>` (non-computed member LHS).\n AssignmentExpression(node) {\n const target = node.left;\n if (\n target.type !== \"MemberExpression\" ||\n target.computed !== false ||\n !isIdentifier(target.property, \"innerHTML\")\n ) {\n return;\n }\n reportIfUnsafe(node.right);\n },\n };\n },\n },\n },\n});\n"],"mappings":"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"}
|
package/dist/utils.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
const isAstNode = (node) => typeof node === "object" && node !== null && "type" in node && typeof node.type === "string" && "range" in node;
|
|
3
|
+
const isIdentifier = (node, name) => {
|
|
4
|
+
if (!isAstNode(node) || node.type !== "Identifier") return false;
|
|
5
|
+
if (typeof node.name !== "string") return false;
|
|
6
|
+
return name === void 0 || node.name === name;
|
|
7
|
+
};
|
|
8
|
+
const isStringLiteral = (node) => isAstNode(node) && node.type === "Literal" && typeof node.value === "string";
|
|
9
|
+
const getPropertyName = (node) => {
|
|
10
|
+
if (isIdentifier(node)) return node.name;
|
|
11
|
+
if (isStringLiteral(node)) return node.value;
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
const getCalleeName = (callee) => {
|
|
15
|
+
if (isIdentifier(callee)) return callee.name;
|
|
16
|
+
if (!isAstNode(callee) || callee.type !== "MemberExpression") return null;
|
|
17
|
+
if (callee.computed !== false) return null;
|
|
18
|
+
const objectName = getCalleeName(callee.object);
|
|
19
|
+
const propertyName = getPropertyName(callee.property);
|
|
20
|
+
if (propertyName === null) return null;
|
|
21
|
+
return objectName === null ? propertyName : `${objectName}.${propertyName}`;
|
|
22
|
+
};
|
|
23
|
+
const unwrapExpression = (node) => {
|
|
24
|
+
if (!isAstNode(node)) return null;
|
|
25
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
|
|
26
|
+
return node;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
Object.defineProperty(exports, "getCalleeName", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function() {
|
|
32
|
+
return getCalleeName;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(exports, "getPropertyName", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function() {
|
|
38
|
+
return getPropertyName;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(exports, "isIdentifier", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: function() {
|
|
44
|
+
return isIdentifier;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(exports, "unwrapExpression", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
get: function() {
|
|
50
|
+
return unwrapExpression;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=utils.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT"}
|
package/dist/utils.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
const isAstNode = (node) => typeof node === "object" && node !== null && "type" in node && typeof node.type === "string" && "range" in node;
|
|
3
|
+
const isIdentifier = (node, name) => {
|
|
4
|
+
if (!isAstNode(node) || node.type !== "Identifier") return false;
|
|
5
|
+
if (typeof node.name !== "string") return false;
|
|
6
|
+
return name === void 0 || node.name === name;
|
|
7
|
+
};
|
|
8
|
+
const isStringLiteral = (node) => isAstNode(node) && node.type === "Literal" && typeof node.value === "string";
|
|
9
|
+
const getPropertyName = (node) => {
|
|
10
|
+
if (isIdentifier(node)) return node.name;
|
|
11
|
+
if (isStringLiteral(node)) return node.value;
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
const getCalleeName = (callee) => {
|
|
15
|
+
if (isIdentifier(callee)) return callee.name;
|
|
16
|
+
if (!isAstNode(callee) || callee.type !== "MemberExpression") return null;
|
|
17
|
+
if (callee.computed !== false) return null;
|
|
18
|
+
const objectName = getCalleeName(callee.object);
|
|
19
|
+
const propertyName = getPropertyName(callee.property);
|
|
20
|
+
if (propertyName === null) return null;
|
|
21
|
+
return objectName === null ? propertyName : `${objectName}.${propertyName}`;
|
|
22
|
+
};
|
|
23
|
+
const unwrapExpression = (node) => {
|
|
24
|
+
if (!isAstNode(node)) return null;
|
|
25
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
|
|
26
|
+
return node;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { unwrapExpression as i, getPropertyName as n, isIdentifier as r, getCalleeName as t };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=utils.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT"}
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stll/oxlint-plugin",
|
|
3
|
+
"version": "0.0.1-placeholder.0",
|
|
4
|
+
"description": "Portable static-safety rules for Oxlint consumers.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/stella/tooling.git",
|
|
9
|
+
"directory": "packages/oxlint-plugin"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.mjs",
|
|
16
|
+
"types": "dist/index.d.mts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"default": "./dist/index.mjs"
|
|
22
|
+
},
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"default": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./no-any-casts": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/no-any-casts.d.mts",
|
|
32
|
+
"default": "./dist/no-any-casts.mjs"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/no-any-casts.d.cts",
|
|
36
|
+
"default": "./dist/no-any-casts.cjs"
|
|
37
|
+
},
|
|
38
|
+
"default": "./dist/no-any-casts.mjs"
|
|
39
|
+
},
|
|
40
|
+
"./no-dangerous-type-assertions": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/no-dangerous-type-assertions.d.mts",
|
|
43
|
+
"default": "./dist/no-dangerous-type-assertions.mjs"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/no-dangerous-type-assertions.d.cts",
|
|
47
|
+
"default": "./dist/no-dangerous-type-assertions.cjs"
|
|
48
|
+
},
|
|
49
|
+
"default": "./dist/no-dangerous-type-assertions.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./no-partial-record-satisfies": {
|
|
52
|
+
"import": {
|
|
53
|
+
"types": "./dist/no-partial-record-satisfies.d.mts",
|
|
54
|
+
"default": "./dist/no-partial-record-satisfies.mjs"
|
|
55
|
+
},
|
|
56
|
+
"require": {
|
|
57
|
+
"types": "./dist/no-partial-record-satisfies.d.cts",
|
|
58
|
+
"default": "./dist/no-partial-record-satisfies.cjs"
|
|
59
|
+
},
|
|
60
|
+
"default": "./dist/no-partial-record-satisfies.mjs"
|
|
61
|
+
},
|
|
62
|
+
"./no-unsafe-inner-html": {
|
|
63
|
+
"import": {
|
|
64
|
+
"types": "./dist/no-unsafe-inner-html.d.mts",
|
|
65
|
+
"default": "./dist/no-unsafe-inner-html.mjs"
|
|
66
|
+
},
|
|
67
|
+
"require": {
|
|
68
|
+
"types": "./dist/no-unsafe-inner-html.d.cts",
|
|
69
|
+
"default": "./dist/no-unsafe-inner-html.cjs"
|
|
70
|
+
},
|
|
71
|
+
"default": "./dist/no-unsafe-inner-html.mjs"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"access": "public"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"format": "oxfmt ."
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@oxlint/plugins": "1.75.0",
|
|
84
|
+
"@stll/typescript-config": "workspace:*",
|
|
85
|
+
"oxfmt": "0.48.0",
|
|
86
|
+
"typescript": "7.0.2"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"@oxlint/plugins": ">=1.75.0 <2",
|
|
90
|
+
"oxlint": ">=1.75.0 <2"
|
|
91
|
+
},
|
|
92
|
+
"engines": {
|
|
93
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
94
|
+
}
|
|
95
|
+
}
|