1
1
'use strict' ;
2
- const { getPropertyName} = require ( '@eslint-community/eslint-utils' ) ;
2
+ const { getPropertyName, ReferenceTracker } = require ( '@eslint-community/eslint-utils' ) ;
3
3
const { fixSpaceAroundKeyword} = require ( './fix/index.js' ) ;
4
4
const { isMemberExpression, isMethodCall} = require ( './ast/index.js' ) ;
5
5
@@ -8,72 +8,144 @@ const messages = {
8
8
'unknown-method' : 'Prefer using method from `{{constructorName}}.prototype`.' ,
9
9
} ;
10
10
11
- /** @param {import('eslint').Rule.RuleContext } context */
12
- function create ( context ) {
11
+ const OBJECT_PROTOTYPE_METHODS = [
12
+ 'hasOwnProperty' ,
13
+ 'isPrototypeOf' ,
14
+ 'propertyIsEnumerable' ,
15
+ 'toLocaleString' ,
16
+ 'toString' ,
17
+ 'valueOf' ,
18
+ ] ;
19
+
20
+ function getConstructorAndMethodName ( methodNode , { sourceCode, globalReferences} ) {
21
+ if ( ! methodNode ) {
22
+ return ;
23
+ }
24
+
25
+ const isGlobalReference = globalReferences . has ( methodNode ) ;
26
+ if ( isGlobalReference ) {
27
+ const path = globalReferences . get ( methodNode ) ;
28
+ return {
29
+ isGlobalReference : true ,
30
+ constructorName : 'Object' ,
31
+ methodName : path . at ( - 1 ) ,
32
+ } ;
33
+ }
34
+
35
+ if ( ! isMemberExpression ( methodNode , { optional : false } ) ) {
36
+ return ;
37
+ }
38
+
39
+ const objectNode = methodNode . object ;
40
+
41
+ if ( ! (
42
+ ( objectNode . type === 'ArrayExpression' && objectNode . elements . length === 0 )
43
+ || ( objectNode . type === 'ObjectExpression' && objectNode . properties . length === 0 )
44
+ ) ) {
45
+ return ;
46
+ }
47
+
48
+ const constructorName = objectNode . type === 'ArrayExpression' ? 'Array' : 'Object' ;
49
+ const methodName = getPropertyName ( methodNode , sourceCode . getScope ( methodNode ) ) ;
50
+
13
51
return {
14
- CallExpression ( callExpression ) {
15
- let methodNode ;
16
-
17
- if (
18
- // `Reflect.apply([].foo, …)`
19
- // `Reflect.apply({}.foo, …)`
20
- isMethodCall ( callExpression , {
21
- object : 'Reflect' ,
22
- method : 'apply' ,
23
- minimumArguments : 1 ,
24
- optionalCall : false ,
25
- optionalMember : false ,
26
- } )
27
- ) {
28
- methodNode = callExpression . arguments [ 0 ] ;
29
- } else if (
30
- // `[].foo.{apply,bind,call}(…)`
31
- // `({}).foo.{apply,bind,call}(…)`
32
- isMethodCall ( callExpression , {
33
- methods : [ 'apply' , 'bind' , 'call' ] ,
34
- optionalCall : false ,
35
- optionalMember : false ,
36
- } )
37
- ) {
38
- methodNode = callExpression . callee . object ;
39
- }
52
+ constructorName,
53
+ methodName,
54
+ } ;
55
+ }
40
56
41
- if ( ! methodNode || ! isMemberExpression ( methodNode , { optional : false } ) ) {
42
- return ;
43
- }
57
+ function getProblem ( callExpression , { sourceCode, globalReferences} ) {
58
+ let methodNode ;
59
+
60
+ if (
61
+ // `Reflect.apply([].foo, …)`
62
+ // `Reflect.apply({}.foo, …)`
63
+ isMethodCall ( callExpression , {
64
+ object : 'Reflect' ,
65
+ method : 'apply' ,
66
+ minimumArguments : 1 ,
67
+ optionalCall : false ,
68
+ optionalMember : false ,
69
+ } )
70
+ ) {
71
+ methodNode = callExpression . arguments [ 0 ] ;
72
+ } else if (
73
+ // `[].foo.{apply,bind,call}(…)`
74
+ // `({}).foo.{apply,bind,call}(…)`
75
+ isMethodCall ( callExpression , {
76
+ methods : [ 'apply' , 'bind' , 'call' ] ,
77
+ optionalCall : false ,
78
+ optionalMember : false ,
79
+ } )
80
+ ) {
81
+ methodNode = callExpression . callee . object ;
82
+ }
44
83
45
- const objectNode = methodNode . object ;
84
+ const {
85
+ isGlobalReference,
86
+ constructorName,
87
+ methodName,
88
+ } = getConstructorAndMethodName ( methodNode , { sourceCode, globalReferences} ) ?? { } ;
46
89
47
- if ( ! (
48
- ( objectNode . type === 'ArrayExpression' && objectNode . elements . length === 0 )
49
- || ( objectNode . type === 'ObjectExpression' && objectNode . properties . length === 0 )
50
- ) ) {
90
+ if ( ! constructorName ) {
91
+ return ;
92
+ }
93
+
94
+ return {
95
+ node : methodNode ,
96
+ messageId : methodName ? 'known-method' : 'unknown-method' ,
97
+ data : { constructorName, methodName} ,
98
+ * fix ( fixer ) {
99
+ if ( isGlobalReference ) {
100
+ yield fixer . replaceText ( methodNode , `${ constructorName } .prototype.${ methodName } ` ) ;
51
101
return ;
52
102
}
53
103
54
- const constructorName = objectNode . type === 'ArrayExpression' ? 'Array' : 'Object' ;
55
- const { sourceCode} = context ;
56
- const methodName = getPropertyName ( methodNode , sourceCode . getScope ( methodNode ) ) ;
57
-
58
- return {
59
- node : methodNode ,
60
- messageId : methodName ? 'known-method' : 'unknown-method' ,
61
- data : { constructorName, methodName} ,
62
- * fix ( fixer ) {
63
- yield fixer . replaceText ( objectNode , `${ constructorName } .prototype` ) ;
64
-
65
- if (
66
- objectNode . type === 'ArrayExpression'
67
- || objectNode . type === 'ObjectExpression'
68
- ) {
69
- yield * fixSpaceAroundKeyword ( fixer , callExpression , sourceCode ) ;
70
- }
71
- } ,
72
- } ;
104
+ if ( isMemberExpression ( methodNode ) ) {
105
+ const objectNode = methodNode . object ;
106
+
107
+ yield fixer . replaceText ( objectNode , `${ constructorName } .prototype` ) ;
108
+
109
+ if (
110
+ objectNode . type === 'ArrayExpression'
111
+ || objectNode . type === 'ObjectExpression'
112
+ ) {
113
+ yield * fixSpaceAroundKeyword ( fixer , callExpression , sourceCode ) ;
114
+ }
115
+ }
73
116
} ,
74
117
} ;
75
118
}
76
119
120
+ /** @param {import('eslint').Rule.RuleContext } context */
121
+ function create ( context ) {
122
+ const { sourceCode} = context ;
123
+ const callExpressions = [ ] ;
124
+
125
+ context . on ( 'CallExpression' , callExpression => {
126
+ callExpressions . push ( callExpression ) ;
127
+ } ) ;
128
+
129
+ context . on ( 'Program:exit' , function * ( program ) {
130
+ const globalReferences = new WeakMap ( ) ;
131
+
132
+ const tracker = new ReferenceTracker ( sourceCode . getScope ( program ) ) ;
133
+
134
+ for ( const { node, path} of tracker . iterateGlobalReferences (
135
+ Object . fromEntries ( OBJECT_PROTOTYPE_METHODS . map ( method => [ method , { [ ReferenceTracker . READ ] : true } ] ) ) ,
136
+ ) ) {
137
+ globalReferences . set ( node , path ) ;
138
+ }
139
+
140
+ for ( const callExpression of callExpressions ) {
141
+ yield getProblem ( callExpression , {
142
+ sourceCode,
143
+ globalReferences,
144
+ } ) ;
145
+ }
146
+ } ) ;
147
+ }
148
+
77
149
/** @type {import('eslint').Rule.RuleModule } */
78
150
module . exports = {
79
151
create,
0 commit comments