11import * as AST from "@eslint-react/ast" ;
22import { isReactHookCall , useComponentCollector } from "@eslint-react/core" ;
33import { getOrElseUpdate } from "@eslint-react/eff" ;
4- import { type RuleContext , type RuleFeature } from "@eslint-react/shared" ;
4+ import { type RuleContext , type RuleFeature , toRegExp } from "@eslint-react/shared" ;
55import { getObjectType } from "@eslint-react/var" ;
6+ import type { TSESTree } from "@typescript-eslint/types" ;
67import { AST_NODE_TYPES as T } from "@typescript-eslint/types" ;
8+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema" ;
79import type { RuleListener } from "@typescript-eslint/utils/ts-eslint" ;
810import type { CamelCase } from "string-ts" ;
911import { match } from "ts-pattern" ;
@@ -16,7 +18,32 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[];
1618
1719export type MessageID = CamelCase < typeof RULE_NAME > ;
1820
19- export default createRule < [ ] , MessageID > ( {
21+ type Options = readonly [
22+ {
23+ safeDefaultProps ?: readonly string [ ] ;
24+ } ,
25+ ] ;
26+
27+ const defaultOptions = [
28+ {
29+ safeDefaultProps : [ ] ,
30+ } ,
31+ ] as const satisfies Options ;
32+
33+ const schema = [
34+ {
35+ type : "object" ,
36+ additionalProperties : false ,
37+ properties : {
38+ safeDefaultProps : {
39+ type : "array" ,
40+ items : { type : "string" } ,
41+ } ,
42+ } ,
43+ } ,
44+ ] satisfies [ JSONSchema4 ] ;
45+
46+ export default createRule < Options , MessageID > ( {
2047 meta : {
2148 type : "problem" ,
2249 docs : {
@@ -27,16 +54,31 @@ export default createRule<[], MessageID>({
2754 noUnstableDefaultProps :
2855 "A/an '{{forbiddenType}}' as default prop. This could lead to potential infinite render loop in React. Use a variable instead of '{{forbiddenType}}'." ,
2956 } ,
30- schema : [ ] ,
57+ schema,
3158 } ,
3259 name : RULE_NAME ,
3360 create,
34- defaultOptions : [ ] ,
61+ defaultOptions,
3562} ) ;
3663
37- export function create ( context : RuleContext < MessageID , [ ] > ) : RuleListener {
64+ function extractIdentifier ( node : TSESTree . Node ) : string | null {
65+ if ( node . type === T . NewExpression && node . callee . type === T . Identifier ) {
66+ return node . callee . name ;
67+ }
68+ if ( node . type === T . CallExpression && node . callee . type === T . MemberExpression ) {
69+ const { object } = node . callee ;
70+ if ( object . type === T . Identifier ) {
71+ return object . name ;
72+ }
73+ }
74+ return null ;
75+ }
76+
77+ export function create ( context : RuleContext < MessageID , Options > , [ options ] : Options ) : RuleListener {
3878 const { ctx, listeners } = useComponentCollector ( context ) ;
3979 const declarators = new WeakMap < AST . TSESTreeFunction , AST . ObjectDestructuringVariableDeclarator [ ] > ( ) ;
80+ const { safeDefaultProps = [ ] } = options ;
81+ const safePatterns = safeDefaultProps . map ( ( s ) => toRegExp ( s ) ) ;
4082
4183 return {
4284 ...listeners ,
@@ -82,6 +124,10 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
82124 if ( isReactHookCall ( construction . node ) ) {
83125 continue ;
84126 }
127+ const identifier = extractIdentifier ( right ) ;
128+ if ( identifier != null && safePatterns . some ( ( pattern ) => pattern . test ( identifier ) ) ) {
129+ continue ;
130+ }
85131 const forbiddenType = AST . toDelimiterFormat ( right ) ;
86132 context . report ( {
87133 messageId : "noUnstableDefaultProps" ,
0 commit comments