uilint-eslint 0.2.100 → 0.2.102
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.d.ts +20 -0
- package/dist/index.js +426 -30
- package/dist/index.js.map +1 -1
- package/dist/rules/no-unsafe-type-casts.js +386 -0
- package/dist/rules/no-unsafe-type-casts.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +22 -0
- package/src/rule-registry.ts +3 -0
- package/src/rules/__fixtures__/no-unsafe-type-casts/unsafe-casts.ts +120 -0
- package/src/rules/no-unsafe-type-casts.test.ts +535 -0
- package/src/rules/no-unsafe-type-casts.ts +510 -0
package/dist/index.d.ts
CHANGED
|
@@ -805,6 +805,16 @@ declare const rules: {
|
|
|
805
805
|
} | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
806
806
|
name: string;
|
|
807
807
|
};
|
|
808
|
+
"no-unsafe-type-casts": _typescript_eslint_utils_ts_eslint.RuleModule<"noAsAny" | "noAsUnknown" | "noDoubleCast" | "noLegacyAsAny" | "noLegacyAsUnknown", [{
|
|
809
|
+
reportAsAny?: boolean;
|
|
810
|
+
reportAsUnknown?: boolean;
|
|
811
|
+
reportDoubleCast?: boolean;
|
|
812
|
+
allowInTestFiles?: boolean;
|
|
813
|
+
allowInCatchBlocks?: boolean;
|
|
814
|
+
allowedTypes?: string[];
|
|
815
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
816
|
+
name: string;
|
|
817
|
+
};
|
|
808
818
|
};
|
|
809
819
|
/**
|
|
810
820
|
* Plugin metadata
|
|
@@ -948,6 +958,16 @@ declare const plugin: {
|
|
|
948
958
|
} | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
949
959
|
name: string;
|
|
950
960
|
};
|
|
961
|
+
"no-unsafe-type-casts": _typescript_eslint_utils_ts_eslint.RuleModule<"noAsAny" | "noAsUnknown" | "noDoubleCast" | "noLegacyAsAny" | "noLegacyAsUnknown", [{
|
|
962
|
+
reportAsAny?: boolean;
|
|
963
|
+
reportAsUnknown?: boolean;
|
|
964
|
+
reportDoubleCast?: boolean;
|
|
965
|
+
allowInTestFiles?: boolean;
|
|
966
|
+
allowInCatchBlocks?: boolean;
|
|
967
|
+
allowedTypes?: string[];
|
|
968
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
969
|
+
name: string;
|
|
970
|
+
};
|
|
951
971
|
};
|
|
952
972
|
};
|
|
953
973
|
/**
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { ESLintUtils } from "@typescript-eslint/utils";
|
|
|
3
3
|
var createRule = ESLintUtils.RuleCreator(
|
|
4
4
|
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
5
5
|
);
|
|
6
|
-
function defineRuleMeta(
|
|
7
|
-
return
|
|
6
|
+
function defineRuleMeta(meta18) {
|
|
7
|
+
return meta18;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// src/rules/consistent-dark-mode.ts
|
|
@@ -4197,8 +4197,8 @@ function loadIndex(projectRoot, indexPath) {
|
|
|
4197
4197
|
log(`Loaded metadata.json: ${Object.keys(entries).length} entries`);
|
|
4198
4198
|
const metadataStore = /* @__PURE__ */ new Map();
|
|
4199
4199
|
const fileToChunks = /* @__PURE__ */ new Map();
|
|
4200
|
-
for (const [id,
|
|
4201
|
-
const m =
|
|
4200
|
+
for (const [id, meta18] of Object.entries(entries)) {
|
|
4201
|
+
const m = meta18;
|
|
4202
4202
|
metadataStore.set(id, {
|
|
4203
4203
|
filePath: m.filePath,
|
|
4204
4204
|
startLine: m.startLine,
|
|
@@ -4302,9 +4302,9 @@ function findSimilarChunks(index, chunkId, threshold) {
|
|
|
4302
4302
|
const sortedAll = allScores.sort((a, b) => b.score - a.score).slice(0, 10);
|
|
4303
4303
|
log(` Top 10 similarity scores (threshold=${threshold}):`);
|
|
4304
4304
|
for (const { id, score } of sortedAll) {
|
|
4305
|
-
const
|
|
4305
|
+
const meta18 = index.metadataStore.get(id);
|
|
4306
4306
|
const meetsThreshold = score >= threshold ? "\u2713" : "\u2717";
|
|
4307
|
-
log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${
|
|
4307
|
+
log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${meta18?.name || "anonymous"} in ${meta18?.filePath})`);
|
|
4308
4308
|
}
|
|
4309
4309
|
log(` Found ${results.length} chunks above threshold`);
|
|
4310
4310
|
return results.sort((a, b) => b.score - a.score);
|
|
@@ -4395,20 +4395,20 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4395
4395
|
log(` Chunk ${chunkId} already reported, skipping`);
|
|
4396
4396
|
continue;
|
|
4397
4397
|
}
|
|
4398
|
-
const
|
|
4399
|
-
if (!
|
|
4398
|
+
const meta18 = index.metadataStore.get(chunkId);
|
|
4399
|
+
if (!meta18) {
|
|
4400
4400
|
log(` No metadata for chunk ${chunkId}`);
|
|
4401
4401
|
continue;
|
|
4402
4402
|
}
|
|
4403
|
-
log(` Checking chunk ${chunkId}: lines ${
|
|
4404
|
-
if (nodeLine >=
|
|
4403
|
+
log(` Checking chunk ${chunkId}: lines ${meta18.startLine}-${meta18.endLine} (node at line ${nodeLine})`);
|
|
4404
|
+
if (nodeLine >= meta18.startLine && nodeLine <= meta18.endLine) {
|
|
4405
4405
|
log(` Node is within chunk range, searching for similar chunks...`);
|
|
4406
4406
|
const similar = findSimilarChunks(index, chunkId, threshold);
|
|
4407
4407
|
if (similar.length > 0) {
|
|
4408
4408
|
const best = similar[0];
|
|
4409
4409
|
const bestMeta = index.metadataStore.get(best.id);
|
|
4410
4410
|
if (bestMeta) {
|
|
4411
|
-
const chunkLines =
|
|
4411
|
+
const chunkLines = meta18.endLine - meta18.startLine + 1;
|
|
4412
4412
|
if (chunkLines < minLines) {
|
|
4413
4413
|
log(` Skipping: chunk has ${chunkLines} lines, below minLines=${minLines}`);
|
|
4414
4414
|
continue;
|
|
@@ -4418,8 +4418,8 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4418
4418
|
const similarity = Math.round(best.score * 100);
|
|
4419
4419
|
const sourceCode = extractCodeFromFile(
|
|
4420
4420
|
filename,
|
|
4421
|
-
|
|
4422
|
-
|
|
4421
|
+
meta18.startLine,
|
|
4422
|
+
meta18.endLine
|
|
4423
4423
|
);
|
|
4424
4424
|
const targetAbsolutePath = join6(projectRoot, bestMeta.filePath);
|
|
4425
4425
|
const targetCode = extractCodeFromFile(
|
|
@@ -4427,17 +4427,17 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4427
4427
|
bestMeta.startLine,
|
|
4428
4428
|
bestMeta.endLine
|
|
4429
4429
|
);
|
|
4430
|
-
log(` REPORTING: ${
|
|
4430
|
+
log(` REPORTING: ${meta18.kind} '${name || meta18.name}' is ${similarity}% similar to '${bestMeta.name}' at ${relPath}:${bestMeta.startLine}`);
|
|
4431
4431
|
context.report({
|
|
4432
4432
|
node,
|
|
4433
4433
|
loc: {
|
|
4434
|
-
start: { line:
|
|
4435
|
-
end: { line:
|
|
4434
|
+
start: { line: meta18.startLine, column: meta18.startColumn },
|
|
4435
|
+
end: { line: meta18.endLine, column: meta18.endColumn }
|
|
4436
4436
|
},
|
|
4437
4437
|
messageId: "semanticDuplicate",
|
|
4438
4438
|
data: {
|
|
4439
|
-
kind:
|
|
4440
|
-
name: name ||
|
|
4439
|
+
kind: meta18.kind,
|
|
4440
|
+
name: name || meta18.name || "(anonymous)",
|
|
4441
4441
|
similarity: String(similarity),
|
|
4442
4442
|
otherName: bestMeta.name || "(anonymous)",
|
|
4443
4443
|
otherLocation: `${relPath}:${bestMeta.startLine}`,
|
|
@@ -4446,10 +4446,10 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4446
4446
|
targetCode: targetCode || "",
|
|
4447
4447
|
sourceLocation: JSON.stringify({
|
|
4448
4448
|
filePath: relativeFilename,
|
|
4449
|
-
startLine:
|
|
4450
|
-
endLine:
|
|
4451
|
-
startColumn:
|
|
4452
|
-
endColumn:
|
|
4449
|
+
startLine: meta18.startLine,
|
|
4450
|
+
endLine: meta18.endLine,
|
|
4451
|
+
startColumn: meta18.startColumn,
|
|
4452
|
+
endColumn: meta18.endColumn
|
|
4453
4453
|
}),
|
|
4454
4454
|
targetLocation: JSON.stringify({
|
|
4455
4455
|
filePath: bestMeta.filePath,
|
|
@@ -4459,7 +4459,7 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4459
4459
|
startColumn: bestMeta.startColumn,
|
|
4460
4460
|
endColumn: bestMeta.endColumn
|
|
4461
4461
|
}),
|
|
4462
|
-
sourceName: name ||
|
|
4462
|
+
sourceName: name || meta18.name || "(anonymous)",
|
|
4463
4463
|
targetName: bestMeta.name || "(anonymous)",
|
|
4464
4464
|
similarityScore: String(best.score)
|
|
4465
4465
|
}
|
|
@@ -4469,7 +4469,7 @@ var no_semantic_duplicates_default = createRule({
|
|
|
4469
4469
|
log(` No similar chunks found above threshold`);
|
|
4470
4470
|
}
|
|
4471
4471
|
} else {
|
|
4472
|
-
log(` Node line ${nodeLine} not in chunk range ${
|
|
4472
|
+
log(` Node line ${nodeLine} not in chunk range ${meta18.startLine}-${meta18.endLine}`);
|
|
4473
4473
|
}
|
|
4474
4474
|
}
|
|
4475
4475
|
}
|
|
@@ -6830,6 +6830,379 @@ var prefer_tailwind_default = createRule({
|
|
|
6830
6830
|
}
|
|
6831
6831
|
});
|
|
6832
6832
|
|
|
6833
|
+
// src/rules/no-unsafe-type-casts.ts
|
|
6834
|
+
var meta16 = defineRuleMeta({
|
|
6835
|
+
id: "no-unsafe-type-casts",
|
|
6836
|
+
version: "1.0.0",
|
|
6837
|
+
name: "No Unsafe Type Casts",
|
|
6838
|
+
description: "Disallow unsafe type casting patterns that bypass TypeScript's type system",
|
|
6839
|
+
defaultSeverity: "error",
|
|
6840
|
+
category: "static",
|
|
6841
|
+
icon: "\u{1F6E1}\uFE0F",
|
|
6842
|
+
hint: "Prevents type system circumvention via casts",
|
|
6843
|
+
defaultEnabled: true,
|
|
6844
|
+
defaultOptions: [
|
|
6845
|
+
{
|
|
6846
|
+
reportAsAny: true,
|
|
6847
|
+
reportAsUnknown: false,
|
|
6848
|
+
reportDoubleCast: true,
|
|
6849
|
+
allowInTestFiles: true,
|
|
6850
|
+
allowInCatchBlocks: true,
|
|
6851
|
+
allowedTypes: []
|
|
6852
|
+
}
|
|
6853
|
+
],
|
|
6854
|
+
optionSchema: {
|
|
6855
|
+
fields: [
|
|
6856
|
+
{
|
|
6857
|
+
key: "reportAsAny",
|
|
6858
|
+
label: "Report 'as any' casts",
|
|
6859
|
+
type: "boolean",
|
|
6860
|
+
defaultValue: true,
|
|
6861
|
+
description: "Flag expressions like `value as any`"
|
|
6862
|
+
},
|
|
6863
|
+
{
|
|
6864
|
+
key: "reportAsUnknown",
|
|
6865
|
+
label: "Report 'as unknown' casts",
|
|
6866
|
+
type: "boolean",
|
|
6867
|
+
defaultValue: false,
|
|
6868
|
+
description: "Flag expressions like `value as unknown`"
|
|
6869
|
+
},
|
|
6870
|
+
{
|
|
6871
|
+
key: "reportDoubleCast",
|
|
6872
|
+
label: "Report double-cast patterns",
|
|
6873
|
+
type: "boolean",
|
|
6874
|
+
defaultValue: true,
|
|
6875
|
+
description: "Flag expressions like `value as unknown as Type`"
|
|
6876
|
+
},
|
|
6877
|
+
{
|
|
6878
|
+
key: "allowInTestFiles",
|
|
6879
|
+
label: "Allow in test files",
|
|
6880
|
+
type: "boolean",
|
|
6881
|
+
defaultValue: true,
|
|
6882
|
+
description: "Skip reporting in *.test.ts, *.spec.ts, and __tests__/* files"
|
|
6883
|
+
},
|
|
6884
|
+
{
|
|
6885
|
+
key: "allowInCatchBlocks",
|
|
6886
|
+
label: "Allow in catch blocks",
|
|
6887
|
+
type: "boolean",
|
|
6888
|
+
defaultValue: true,
|
|
6889
|
+
description: "Allow type casts inside catch blocks for error handling"
|
|
6890
|
+
},
|
|
6891
|
+
{
|
|
6892
|
+
key: "allowedTypes",
|
|
6893
|
+
label: "Allowed target types",
|
|
6894
|
+
type: "text",
|
|
6895
|
+
defaultValue: "",
|
|
6896
|
+
placeholder: "HTMLElement, Error, Event",
|
|
6897
|
+
description: "Comma-separated list of type names that are allowed as cast targets"
|
|
6898
|
+
}
|
|
6899
|
+
]
|
|
6900
|
+
},
|
|
6901
|
+
docs: `
|
|
6902
|
+
## What it does
|
|
6903
|
+
|
|
6904
|
+
Detects and prevents unsafe type casting patterns in TypeScript that bypass
|
|
6905
|
+
the type system. These patterns can introduce runtime errors by lying to the
|
|
6906
|
+
compiler about types.
|
|
6907
|
+
|
|
6908
|
+
### Detected Patterns
|
|
6909
|
+
|
|
6910
|
+
| Pattern | Risk | Description |
|
|
6911
|
+
|---------|------|-------------|
|
|
6912
|
+
| \`x as any\` | High | Completely disables type checking |
|
|
6913
|
+
| \`x as unknown as T\` | High | Forces incompatible type conversion |
|
|
6914
|
+
| \`x as unknown\` | Medium | Casts to unknown without narrowing |
|
|
6915
|
+
| \`<any>x\` | High | Legacy syntax equivalent to \`as any\` |
|
|
6916
|
+
|
|
6917
|
+
## Why it's useful
|
|
6918
|
+
|
|
6919
|
+
- **Type Safety**: Catches type system circumvention that leads to runtime errors
|
|
6920
|
+
- **Code Quality**: Encourages proper type guards and runtime validation
|
|
6921
|
+
- **Maintainability**: Makes type assumptions explicit and verifiable
|
|
6922
|
+
- **Refactoring**: Prevents hidden type mismatches that break during refactors
|
|
6923
|
+
|
|
6924
|
+
## Examples
|
|
6925
|
+
|
|
6926
|
+
### \u274C Incorrect
|
|
6927
|
+
|
|
6928
|
+
\`\`\`typescript
|
|
6929
|
+
// Casting to any - bypasses all type checking
|
|
6930
|
+
const data = response.data as any;
|
|
6931
|
+
data.nonExistentMethod(); // No error, but crashes at runtime
|
|
6932
|
+
|
|
6933
|
+
// Double-cast pattern - forces incompatible types
|
|
6934
|
+
const user = jsonData as unknown as User;
|
|
6935
|
+
console.log(user.email); // Might be undefined!
|
|
6936
|
+
|
|
6937
|
+
// Legacy angle-bracket syntax
|
|
6938
|
+
const element = <any>document.getElementById("app");
|
|
6939
|
+
\`\`\`
|
|
6940
|
+
|
|
6941
|
+
### \u2705 Correct
|
|
6942
|
+
|
|
6943
|
+
\`\`\`typescript
|
|
6944
|
+
// Use runtime validation (Zod, io-ts, etc.)
|
|
6945
|
+
const user = UserSchema.parse(jsonData);
|
|
6946
|
+
console.log(user.email); // Type-safe!
|
|
6947
|
+
|
|
6948
|
+
// Use type guards
|
|
6949
|
+
function isUser(data: unknown): data is User {
|
|
6950
|
+
return typeof data === "object" && data !== null && "email" in data;
|
|
6951
|
+
}
|
|
6952
|
+
|
|
6953
|
+
if (isUser(jsonData)) {
|
|
6954
|
+
console.log(jsonData.email); // Narrowed to User
|
|
6955
|
+
}
|
|
6956
|
+
|
|
6957
|
+
// Proper DOM type assertions (allowed by default)
|
|
6958
|
+
const input = document.getElementById("email") as HTMLInputElement;
|
|
6959
|
+
|
|
6960
|
+
// Safe cast with validation
|
|
6961
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
6962
|
+
\`\`\`
|
|
6963
|
+
|
|
6964
|
+
## Configuration
|
|
6965
|
+
|
|
6966
|
+
\`\`\`js
|
|
6967
|
+
// eslint.config.js
|
|
6968
|
+
"uilint/no-unsafe-type-casts": ["error", {
|
|
6969
|
+
reportAsAny: true, // Flag 'as any' casts
|
|
6970
|
+
reportAsUnknown: false, // Don't flag 'as unknown' alone
|
|
6971
|
+
reportDoubleCast: true, // Flag 'as unknown as T' patterns
|
|
6972
|
+
allowInTestFiles: true, // Allow in test files
|
|
6973
|
+
allowInCatchBlocks: true, // Allow error handling casts
|
|
6974
|
+
allowedTypes: [ // Types that are safe to cast to
|
|
6975
|
+
"HTMLElement",
|
|
6976
|
+
"HTMLInputElement",
|
|
6977
|
+
"HTMLButtonElement",
|
|
6978
|
+
"Error",
|
|
6979
|
+
"Event"
|
|
6980
|
+
]
|
|
6981
|
+
}]
|
|
6982
|
+
\`\`\`
|
|
6983
|
+
|
|
6984
|
+
## Allowed Type Exceptions
|
|
6985
|
+
|
|
6986
|
+
You can configure specific types that are allowed as cast targets. This is useful for:
|
|
6987
|
+
|
|
6988
|
+
- **DOM elements**: \`HTMLElement\`, \`HTMLInputElement\`, etc.
|
|
6989
|
+
- **Error handling**: \`Error\`, \`TypeError\`, etc.
|
|
6990
|
+
- **Event handling**: \`Event\`, \`MouseEvent\`, \`KeyboardEvent\`, etc.
|
|
6991
|
+
- **Third-party types**: Types from libraries that require casting
|
|
6992
|
+
|
|
6993
|
+
## When to Disable
|
|
6994
|
+
|
|
6995
|
+
Consider disabling this rule when:
|
|
6996
|
+
|
|
6997
|
+
- Working with legacy code that requires extensive type casting
|
|
6998
|
+
- Integrating with untyped JavaScript libraries
|
|
6999
|
+
- Writing type-level tests or type utilities
|
|
7000
|
+
|
|
7001
|
+
Use inline comments for specific exceptions:
|
|
7002
|
+
|
|
7003
|
+
\`\`\`typescript
|
|
7004
|
+
// eslint-disable-next-line uilint/no-unsafe-type-casts
|
|
7005
|
+
const data = legacyApiResponse as any;
|
|
7006
|
+
\`\`\`
|
|
7007
|
+
`
|
|
7008
|
+
});
|
|
7009
|
+
function isTestFile2(filename) {
|
|
7010
|
+
return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.includes("__mocks__");
|
|
7011
|
+
}
|
|
7012
|
+
function isInsideCatchBlock(node) {
|
|
7013
|
+
let current = node.parent;
|
|
7014
|
+
while (current) {
|
|
7015
|
+
if (current.type === "CatchClause") {
|
|
7016
|
+
return true;
|
|
7017
|
+
}
|
|
7018
|
+
current = current.parent;
|
|
7019
|
+
}
|
|
7020
|
+
return false;
|
|
7021
|
+
}
|
|
7022
|
+
function getTypeName(typeAnnotation) {
|
|
7023
|
+
switch (typeAnnotation.type) {
|
|
7024
|
+
case "TSTypeReference":
|
|
7025
|
+
if (typeAnnotation.typeName.type === "Identifier") {
|
|
7026
|
+
return typeAnnotation.typeName.name;
|
|
7027
|
+
}
|
|
7028
|
+
if (typeAnnotation.typeName.type === "TSQualifiedName") {
|
|
7029
|
+
return `${getQualifiedName(typeAnnotation.typeName)}`;
|
|
7030
|
+
}
|
|
7031
|
+
return null;
|
|
7032
|
+
case "TSAnyKeyword":
|
|
7033
|
+
return "any";
|
|
7034
|
+
case "TSUnknownKeyword":
|
|
7035
|
+
return "unknown";
|
|
7036
|
+
default:
|
|
7037
|
+
return null;
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
function getQualifiedName(node) {
|
|
7041
|
+
const left = node.left.type === "Identifier" ? node.left.name : getQualifiedName(node.left);
|
|
7042
|
+
return `${left}.${node.right.name}`;
|
|
7043
|
+
}
|
|
7044
|
+
function isAllowedType(typeAnnotation, allowedTypes) {
|
|
7045
|
+
const typeName = getTypeName(typeAnnotation);
|
|
7046
|
+
if (!typeName) return false;
|
|
7047
|
+
return allowedTypes.includes(typeName);
|
|
7048
|
+
}
|
|
7049
|
+
function isDoubleCastPattern(node) {
|
|
7050
|
+
if (node.expression.type !== "TSAsExpression") {
|
|
7051
|
+
return false;
|
|
7052
|
+
}
|
|
7053
|
+
const innerCast = node.expression;
|
|
7054
|
+
const innerType = innerCast.typeAnnotation;
|
|
7055
|
+
return innerType.type === "TSUnknownKeyword" || innerType.type === "TSAnyKeyword";
|
|
7056
|
+
}
|
|
7057
|
+
function isLegacyDoubleCastPattern(node) {
|
|
7058
|
+
if (node.expression.type !== "TSTypeAssertion") {
|
|
7059
|
+
return false;
|
|
7060
|
+
}
|
|
7061
|
+
const innerCast = node.expression;
|
|
7062
|
+
const innerType = innerCast.typeAnnotation;
|
|
7063
|
+
return innerType.type === "TSUnknownKeyword" || innerType.type === "TSAnyKeyword";
|
|
7064
|
+
}
|
|
7065
|
+
var no_unsafe_type_casts_default = createRule({
|
|
7066
|
+
name: "no-unsafe-type-casts",
|
|
7067
|
+
meta: {
|
|
7068
|
+
type: "problem",
|
|
7069
|
+
docs: {
|
|
7070
|
+
description: "Disallow unsafe type casting patterns that bypass TypeScript's type system"
|
|
7071
|
+
},
|
|
7072
|
+
messages: {
|
|
7073
|
+
noAsAny: "Avoid using 'as any'. This completely bypasses type checking. Use proper typing, type guards, or runtime validation instead.",
|
|
7074
|
+
noAsUnknown: "Casting to 'unknown' without subsequent type narrowing bypasses type safety. Use type guards or validation.",
|
|
7075
|
+
noDoubleCast: "Double-cast pattern ('as unknown as {{targetType}}') bypasses type checking. Use runtime validation or type guards instead.",
|
|
7076
|
+
noLegacyAsAny: "Avoid using '<any>' type assertion. This completely bypasses type checking. Use proper typing or type guards instead.",
|
|
7077
|
+
noLegacyAsUnknown: "Casting to '<unknown>' without subsequent type narrowing bypasses type safety. Use type guards or validation."
|
|
7078
|
+
},
|
|
7079
|
+
schema: [
|
|
7080
|
+
{
|
|
7081
|
+
type: "object",
|
|
7082
|
+
properties: {
|
|
7083
|
+
reportAsAny: {
|
|
7084
|
+
type: "boolean",
|
|
7085
|
+
description: "Report 'as any' casts"
|
|
7086
|
+
},
|
|
7087
|
+
reportAsUnknown: {
|
|
7088
|
+
type: "boolean",
|
|
7089
|
+
description: "Report 'as unknown' casts"
|
|
7090
|
+
},
|
|
7091
|
+
reportDoubleCast: {
|
|
7092
|
+
type: "boolean",
|
|
7093
|
+
description: "Report double-cast patterns like 'as unknown as T'"
|
|
7094
|
+
},
|
|
7095
|
+
allowInTestFiles: {
|
|
7096
|
+
type: "boolean",
|
|
7097
|
+
description: "Allow casts in test files"
|
|
7098
|
+
},
|
|
7099
|
+
allowInCatchBlocks: {
|
|
7100
|
+
type: "boolean",
|
|
7101
|
+
description: "Allow casts in catch blocks for error handling"
|
|
7102
|
+
},
|
|
7103
|
+
allowedTypes: {
|
|
7104
|
+
type: "array",
|
|
7105
|
+
items: { type: "string" },
|
|
7106
|
+
description: "Type names that are allowed as cast targets"
|
|
7107
|
+
}
|
|
7108
|
+
},
|
|
7109
|
+
additionalProperties: false
|
|
7110
|
+
}
|
|
7111
|
+
]
|
|
7112
|
+
},
|
|
7113
|
+
defaultOptions: [
|
|
7114
|
+
{
|
|
7115
|
+
reportAsAny: true,
|
|
7116
|
+
reportAsUnknown: false,
|
|
7117
|
+
reportDoubleCast: true,
|
|
7118
|
+
allowInTestFiles: true,
|
|
7119
|
+
allowInCatchBlocks: true,
|
|
7120
|
+
allowedTypes: []
|
|
7121
|
+
}
|
|
7122
|
+
],
|
|
7123
|
+
create(context) {
|
|
7124
|
+
const options = context.options[0] || {};
|
|
7125
|
+
const reportAsAny = options.reportAsAny ?? true;
|
|
7126
|
+
const reportAsUnknown = options.reportAsUnknown ?? false;
|
|
7127
|
+
const reportDoubleCast = options.reportDoubleCast ?? true;
|
|
7128
|
+
const allowInTestFiles = options.allowInTestFiles ?? true;
|
|
7129
|
+
const allowInCatchBlocks = options.allowInCatchBlocks ?? true;
|
|
7130
|
+
const allowedTypes = options.allowedTypes ?? [];
|
|
7131
|
+
const filename = context.filename || context.getFilename();
|
|
7132
|
+
if (allowInTestFiles && isTestFile2(filename)) {
|
|
7133
|
+
return {};
|
|
7134
|
+
}
|
|
7135
|
+
function checkAsExpression(node) {
|
|
7136
|
+
const targetType = node.typeAnnotation;
|
|
7137
|
+
if (allowInCatchBlocks && isInsideCatchBlock(node)) {
|
|
7138
|
+
return;
|
|
7139
|
+
}
|
|
7140
|
+
if (reportDoubleCast && isDoubleCastPattern(node)) {
|
|
7141
|
+
const targetTypeName = getTypeName(targetType) || "Type";
|
|
7142
|
+
context.report({
|
|
7143
|
+
node,
|
|
7144
|
+
messageId: "noDoubleCast",
|
|
7145
|
+
data: { targetType: targetTypeName }
|
|
7146
|
+
});
|
|
7147
|
+
return;
|
|
7148
|
+
}
|
|
7149
|
+
if (isAllowedType(targetType, allowedTypes)) {
|
|
7150
|
+
return;
|
|
7151
|
+
}
|
|
7152
|
+
if (reportAsAny && targetType.type === "TSAnyKeyword") {
|
|
7153
|
+
context.report({
|
|
7154
|
+
node,
|
|
7155
|
+
messageId: "noAsAny"
|
|
7156
|
+
});
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
if (reportAsUnknown && targetType.type === "TSUnknownKeyword") {
|
|
7160
|
+
context.report({
|
|
7161
|
+
node,
|
|
7162
|
+
messageId: "noAsUnknown"
|
|
7163
|
+
});
|
|
7164
|
+
return;
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
function checkTypeAssertion(node) {
|
|
7168
|
+
const targetType = node.typeAnnotation;
|
|
7169
|
+
if (allowInCatchBlocks && isInsideCatchBlock(node)) {
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
if (reportDoubleCast && isLegacyDoubleCastPattern(node)) {
|
|
7173
|
+
const targetTypeName = getTypeName(targetType) || "Type";
|
|
7174
|
+
context.report({
|
|
7175
|
+
node,
|
|
7176
|
+
messageId: "noDoubleCast",
|
|
7177
|
+
data: { targetType: targetTypeName }
|
|
7178
|
+
});
|
|
7179
|
+
return;
|
|
7180
|
+
}
|
|
7181
|
+
if (isAllowedType(targetType, allowedTypes)) {
|
|
7182
|
+
return;
|
|
7183
|
+
}
|
|
7184
|
+
if (reportAsAny && targetType.type === "TSAnyKeyword") {
|
|
7185
|
+
context.report({
|
|
7186
|
+
node,
|
|
7187
|
+
messageId: "noLegacyAsAny"
|
|
7188
|
+
});
|
|
7189
|
+
return;
|
|
7190
|
+
}
|
|
7191
|
+
if (reportAsUnknown && targetType.type === "TSUnknownKeyword") {
|
|
7192
|
+
context.report({
|
|
7193
|
+
node,
|
|
7194
|
+
messageId: "noLegacyAsUnknown"
|
|
7195
|
+
});
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
return {
|
|
7200
|
+
TSAsExpression: checkAsExpression,
|
|
7201
|
+
TSTypeAssertion: checkTypeAssertion
|
|
7202
|
+
};
|
|
7203
|
+
}
|
|
7204
|
+
});
|
|
7205
|
+
|
|
6833
7206
|
// src/category-registry.ts
|
|
6834
7207
|
var categoryRegistry = [
|
|
6835
7208
|
{
|
|
@@ -6873,7 +7246,9 @@ var ruleRegistry = [
|
|
|
6873
7246
|
// Test coverage enforcement
|
|
6874
7247
|
meta14,
|
|
6875
7248
|
// Style preferences
|
|
6876
|
-
meta15
|
|
7249
|
+
meta15,
|
|
7250
|
+
// Type safety
|
|
7251
|
+
meta16
|
|
6877
7252
|
];
|
|
6878
7253
|
function getRuleMetadata(id) {
|
|
6879
7254
|
return ruleRegistry.find((rule) => rule.id === id);
|
|
@@ -6906,15 +7281,16 @@ var rules = {
|
|
|
6906
7281
|
"require-input-validation": require_input_validation_default,
|
|
6907
7282
|
"no-semantic-duplicates": no_semantic_duplicates_default,
|
|
6908
7283
|
"require-test-coverage": require_test_coverage_default,
|
|
6909
|
-
"prefer-tailwind": prefer_tailwind_default
|
|
7284
|
+
"prefer-tailwind": prefer_tailwind_default,
|
|
7285
|
+
"no-unsafe-type-casts": no_unsafe_type_casts_default
|
|
6910
7286
|
};
|
|
6911
7287
|
var version = "0.1.0";
|
|
6912
|
-
var
|
|
7288
|
+
var meta17 = {
|
|
6913
7289
|
name: "uilint",
|
|
6914
7290
|
version
|
|
6915
7291
|
};
|
|
6916
7292
|
var plugin = {
|
|
6917
|
-
meta:
|
|
7293
|
+
meta: meta17,
|
|
6918
7294
|
rules
|
|
6919
7295
|
};
|
|
6920
7296
|
var jsxLanguageOptions = {
|
|
@@ -7049,6 +7425,16 @@ var recommendedConfig = {
|
|
|
7049
7425
|
"preferSemanticColors": true,
|
|
7050
7426
|
"allowedHardCodedColors": []
|
|
7051
7427
|
}
|
|
7428
|
+
]],
|
|
7429
|
+
"uilint/no-unsafe-type-casts": ["error", ...[
|
|
7430
|
+
{
|
|
7431
|
+
"reportAsAny": true,
|
|
7432
|
+
"reportAsUnknown": false,
|
|
7433
|
+
"reportDoubleCast": true,
|
|
7434
|
+
"allowInTestFiles": true,
|
|
7435
|
+
"allowInCatchBlocks": true,
|
|
7436
|
+
"allowedTypes": []
|
|
7437
|
+
}
|
|
7052
7438
|
]]
|
|
7053
7439
|
}
|
|
7054
7440
|
};
|
|
@@ -7196,6 +7582,16 @@ var strictConfig = {
|
|
|
7196
7582
|
"preferSemanticColors": true,
|
|
7197
7583
|
"allowedHardCodedColors": []
|
|
7198
7584
|
}
|
|
7585
|
+
]],
|
|
7586
|
+
"uilint/no-unsafe-type-casts": ["error", ...[
|
|
7587
|
+
{
|
|
7588
|
+
"reportAsAny": true,
|
|
7589
|
+
"reportAsUnknown": false,
|
|
7590
|
+
"reportDoubleCast": true,
|
|
7591
|
+
"allowInTestFiles": true,
|
|
7592
|
+
"allowInCatchBlocks": true,
|
|
7593
|
+
"allowedTypes": []
|
|
7594
|
+
}
|
|
7199
7595
|
]]
|
|
7200
7596
|
}
|
|
7201
7597
|
};
|
|
@@ -7204,7 +7600,7 @@ var configs = {
|
|
|
7204
7600
|
strict: strictConfig
|
|
7205
7601
|
};
|
|
7206
7602
|
var uilintEslint = {
|
|
7207
|
-
meta:
|
|
7603
|
+
meta: meta17,
|
|
7208
7604
|
plugin,
|
|
7209
7605
|
rules,
|
|
7210
7606
|
configs
|
|
@@ -7242,7 +7638,7 @@ export {
|
|
|
7242
7638
|
isEventHandlerAttribute,
|
|
7243
7639
|
loadCache,
|
|
7244
7640
|
loadStyleguide,
|
|
7245
|
-
|
|
7641
|
+
meta17 as meta,
|
|
7246
7642
|
plugin,
|
|
7247
7643
|
ruleRegistry,
|
|
7248
7644
|
rules,
|