11/**
2- * @fileoverview Don't allow debug() to be committed to the repository.
2+ * @fileoverview Don't allow debug() to be committed to the repository.
33 * @author Ben Monro
4+ * @author codejedi365
45 */
5- "use strict" ;
6-
6+ import {
7+ CallExpression ,
8+ MemberExpression ,
9+ Identifier ,
10+ AST_NODE_TYPES ,
11+ BaseNode
12+ } from "@typescript-eslint/types/dist/ast-spec" ;
713import { createRule } from "../create-rule" ;
814
15+ type FunctionName = string ;
16+ type ObjectName = string ;
17+
18+ const testFnAttributes = [
19+ // Derived List from TestFn class of [email protected] /ts-defs/index.d.ts 20+ // - only extracted attributes which return the testFn object (this)
21+ // which are possible modifiers to a test call before the test callback
22+ // is defined
23+ "only" ,
24+ "skip" ,
25+ "disablePageCaching" ,
26+ "disablePageReloads"
27+ ] ;
28+
29+ function isMemberExpression ( node : BaseNode ) : node is MemberExpression {
30+ return node . type === AST_NODE_TYPES . MemberExpression ;
31+ }
32+
33+ function isIdentifier ( node : BaseNode ) : node is Identifier {
34+ return node . type === AST_NODE_TYPES . Identifier ;
35+ }
36+
37+ function isCallExpression ( node : BaseNode ) : node is CallExpression {
38+ return node . type === AST_NODE_TYPES . CallExpression ;
39+ }
40+
41+ function digForIdentifierName ( startNode : BaseNode ) : string {
42+ function checkTypeForRecursion (
43+ node : BaseNode
44+ ) : node is CallExpression | MemberExpression | Identifier {
45+ return (
46+ isIdentifier ( node ) ||
47+ isMemberExpression ( node ) ||
48+ isCallExpression ( node )
49+ ) ;
50+ }
51+ function deriveFnName (
52+ node : CallExpression | MemberExpression | Identifier
53+ ) : string {
54+ let nextNode : BaseNode = node ;
55+
56+ if ( isCallExpression ( node ) ) {
57+ nextNode = node . callee ;
58+ } else if ( isMemberExpression ( node ) ) {
59+ nextNode = node . object ;
60+ } else if ( isIdentifier ( node ) ) {
61+ return node . name ;
62+ }
63+
64+ if ( ! checkTypeForRecursion ( nextNode ) ) throw new Error ( ) ;
65+ return deriveFnName ( nextNode ) ;
66+ }
67+
68+ // Start Point
69+ try {
70+ if ( ! checkTypeForRecursion ( startNode ) ) throw new Error ( ) ;
71+ return deriveFnName ( startNode ) ;
72+ } catch ( e ) {
73+ throw new Error ( "Could not derive function name from callee." ) ;
74+ }
75+ }
76+
77+ function deriveFunctionName ( fnCall : CallExpression ) : string {
78+ const startNode =
79+ isMemberExpression ( fnCall . callee ) &&
80+ isIdentifier ( fnCall . callee . property )
81+ ? fnCall . callee . property
82+ : fnCall . callee ;
83+
84+ return digForIdentifierName ( startNode ) ;
85+ }
86+
87+ /**
88+ * Must detect symbol names in the following syntatical situations
89+ * 1. stand-alone function call (identifier only)
90+ * 2. object class method call (MemberExpression)
91+ * 3. n+ deep object attributes (Recursive MemberExpressions)
92+ * 4. when expression Is on a method chain (Recursive CallExpressions)
93+ * @param fnCall
94+ * @returns top level symbol for name of object
95+ */
96+ function deriveObjectName ( fnCall : CallExpression ) : string {
97+ return digForIdentifierName ( fnCall . callee ) ;
98+ }
99+
100+ function determineCodeLocation (
101+ node : CallExpression
102+ ) : [ FunctionName , ObjectName ] {
103+ return [ deriveFunctionName ( node ) , deriveObjectName ( node ) ] ;
104+ }
105+
9106//------------------------------------------------------------------------------
10107// Rule Definition
11108//------------------------------------------------------------------------------
@@ -14,9 +111,9 @@ export default createRule({
14111 name : __filename ,
15112 defaultOptions : [ ] ,
16113 meta : {
17- type :"problem" ,
114+ type : "problem" ,
18115 messages : {
19- missingExpect : ' Please ensure your test has at least one expect'
116+ missingExpect : " Please ensure your test has at least one expect"
20117 } ,
21118 docs : {
22119 description : "Ensure tests have at least one expect" ,
@@ -25,35 +122,67 @@ export default createRule({
25122 } ,
26123 schema : [ ]
27124 } ,
28- create : function ( context ) {
29-
30- let hasExpect = false ;
31- let isInsideTest = false ;
125+ create ( context ) {
126+ let hasExpect = false ;
127+ let isInsideTest = false ;
128+ let ignoreExpects = false ;
32129 return {
33- "CallExpression" ( node : any ) {
34- const name = node . callee . name || node . callee . property ?. name ;
35- const objectName = node . callee . object ?. name || node . callee . callee ?. object ?. object ?. name || node . parent . callee ?. callee ?. object ?. name ;
36- if ( name === "test" || objectName === "test" ) {
37- isInsideTest = true ;
130+ "CallExpression" : ( node : CallExpression ) => {
131+ if ( isInsideTest && hasExpect ) return ; // Short circuit, already found
132+
133+ let fnName ;
134+ let objectName ;
135+ try {
136+ [ fnName , objectName ] = determineCodeLocation ( node ) ;
137+ } catch ( e ) {
138+ // ABORT: Failed to evaluate rule effectively
139+ // since I cannot derive values to determine location in the code
140+ return ;
38141 }
39- if ( isInsideTest && name === "expect" ) {
40- hasExpect = true ;
142+
143+ if ( isInsideTest ) {
144+ if ( ignoreExpects ) return ;
145+ if ( fnName === "expect" ) {
146+ hasExpect = true ;
147+ return ;
148+ }
149+ if ( objectName === "test" ) {
150+ // only happens in chained methods with internal callbacks
151+ // like test.before(() => {})("my test", async () => {})
152+ // prevents any registering of an expect in the before() callback
153+ ignoreExpects = true ;
154+ }
155+ return ;
41156 }
42- } ,
43-
44- "CallExpression:exit" ( node : any ) {
45- const name = node . callee . name || node . callee . property ?. name ;
46-
47- const objectName = node . callee . object ?. name || node . callee . callee ?. object ?. object ?. name || node . parent . callee ?. callee . object . name ;
48- if ( name === "test" || objectName === "test" ) {
49- if ( ! hasExpect ) {
50- context . report ( { node, messageId : "missingExpect" } ) ;
51- }
52- hasExpect = false ;
53- isInsideTest = false ;
157+ // Determine if inside/chained to a test() function
158+ if ( objectName !== "test" ) return ;
159+ if ( fnName === "test" || testFnAttributes . includes ( fnName ) ) {
160+ isInsideTest = true ;
54161 }
55- }
56- }
162+ } ,
163+
164+ "CallExpression:exit" : ( node : CallExpression ) => {
165+ if ( ! isInsideTest ) return ; // Short circuit
166+
167+ let fnName ;
168+ let objectName ;
169+ try {
170+ [ fnName , objectName ] = determineCodeLocation ( node ) ;
171+ } catch ( e ) {
172+ // ABORT: Failed to evaluate rule effectively
173+ // since I cannot derive values to determine location in the code
174+ return ;
175+ }
176+ if ( objectName !== "test" ) return ;
177+ if ( fnName === "test" || testFnAttributes . includes ( fnName ) ) {
178+ if ( ! hasExpect ) {
179+ context . report ( { node, messageId : "missingExpect" } ) ;
180+ }
181+ hasExpect = false ;
182+ isInsideTest = false ;
183+ }
184+ ignoreExpects = false ;
185+ }
186+ } ;
57187 }
58- }
59- ) ;
188+ } ) ;
0 commit comments