3
3
4
4
import * as ts from 'ts-morph' ;
5
5
import path from 'path' ;
6
+ import chalk from 'chalk' ;
6
7
import { getExpressionChain } from './utils' ;
7
8
8
9
const LIB_DIR = path . normalize ( __dirname ) ;
@@ -28,8 +29,8 @@ function throwError(
28
29
strPath : string ,
29
30
mutationType : 'function' | 'assignment'
30
31
) {
31
- const mutationLine = getNodeLines ( mutation , strPath ) ;
32
- const accessLine = getNodeLines ( access , strPath ) ;
32
+ const mutationLine = getNodeLines ( mutation , strPath , 'yellow' ) ;
33
+ const accessLine = getNodeLines ( access , strPath , 'red' ) ;
33
34
34
35
if ( mutationType === 'assignment' ) {
35
36
const refRhs =
@@ -39,18 +40,22 @@ function throwError(
39
40
40
41
if ( refRhs === undefined ) {
41
42
throw Error (
42
- `Attempted to access "${ access . getText ( ) } " which may have been mutated.\nMutation: ${ mutationLine } \nAccessed: ${ accessLine } `
43
+ `Attempted to access "${ access . getText ( ) } " which may have been mutated.
44
+
45
+ ${ mutationLine } Value mutated here.
46
+
47
+ ${ accessLine } Attempted access of the value ocurred here`
43
48
) ;
44
49
}
45
50
46
51
const aliasInRef = refRhs . isKind ( ts . SyntaxKind . Identifier )
47
52
? refRhs
48
53
: refRhs . getDescendants ( ) . find ( ( d ) => aliases . map ( ( a ) => a . getText ( ) ) . includes ( d . getText ( ) ) ) ! ;
49
- const staleRefLine = getNodeLines ( aliasInRef ! , strPath ) ;
54
+ const staleRefLine = getNodeLines ( aliasInRef ! , strPath , 'yellow' ) ;
50
55
throw Error (
51
56
`Attempted to access "${ access . getText ( ) } " which may have been mutated. You might want to use clone in the initial assignment
52
57
53
- ${ staleRefLine } Initial assignment here. Suggestion: clone(${ aliasInRef . getText ( ) } )
58
+ ${ staleRefLine } Initial assignment here. ${ chalk . bold . underline . yellow ( ' Suggestion' ) } : clone(${ aliasInRef . getText ( ) } )
54
59
55
60
${ mutationLine } Mutation of the value ocurred here.
56
61
@@ -63,7 +68,9 @@ ${accessLine} Attempted access of the value occured here.
63
68
throw Error (
64
69
`Attempted to access "${ access . getText ( ) } " after it was passed to a function. You probably want to use clone in the function call
65
70
66
- Function Call: ${ mutationLine } Value passed to function here. Suggestion: clone(${ access . getText ( ) } )
71
+ Function Call: ${ mutationLine } Value passed to function here. ${ chalk . bold . underline . yellow (
72
+ 'Suggestion'
73
+ ) } : clone(${ access . getText ( ) } )
67
74
68
75
${ accessLine } Attempted access to value after passed to function here
69
76
`
@@ -101,7 +108,33 @@ function includesNode(haystack: ts.Node, needle: ts.Node): boolean {
101
108
} ) ;
102
109
}
103
110
104
- function getNodeLines ( node : ts . Node , pathStr : string ) {
111
+ /**
112
+ * Colors a specific range of characters in a string using chalk
113
+ * @param text The input string to color
114
+ * @param startIndex The starting index of the range to color (inclusive)
115
+ * @param endIndex The ending index of the range to color (exclusive)
116
+ * @param color The chalk color to apply (e.g., 'red', 'blue', 'green')
117
+ * @returns The string with the specified range colored
118
+ */
119
+ function colorRange ( text : string , startIndex : number , endIndex : number , color : 'yellow' | 'red' ) : string {
120
+ // Validate indices
121
+ if ( startIndex < 0 || endIndex > text . length || startIndex >= endIndex ) {
122
+ throw new Error ( 'Invalid range indices' ) ;
123
+ }
124
+
125
+ // Get the parts of the string
126
+ const beforeRange = text . substring ( 0 , startIndex ) ;
127
+ const coloredPart = text . substring ( startIndex , endIndex ) ;
128
+ const afterRange = text . substring ( endIndex ) ;
129
+
130
+ // Apply color using chalk's dynamic method access
131
+ const coloredText = chalk [ color ] ( coloredPart ) ;
132
+
133
+ // Combine all parts
134
+ return beforeRange + coloredText + afterRange ;
135
+ }
136
+
137
+ function getNodeLines ( node : ts . Node , pathStr : string , nodeColor : 'yellow' | 'red' ) {
105
138
const nonTrimmedLine = node . getSourceFile ( ) . getFullText ( ) . split ( '\n' ) [ node . getStartLineNumber ( ) - 1 ] ;
106
139
const refFullLine = nonTrimmedLine . trim ( ) ;
107
140
const char = ts . ts . getLineAndCharacterOfPosition (
@@ -110,9 +143,12 @@ function getNodeLines(node: ts.Node, pathStr: string) {
110
143
) . character ;
111
144
const nodeOffset = char - ( nonTrimmedLine . length - refFullLine . length ) ;
112
145
113
- return `${ pathStr } :${ node . getStartLineNumber ( ) } :${ char } \n ${ refFullLine } \n ${ ' ' . repeat ( nodeOffset ) } ${ '^' . repeat (
114
- node . getText ( ) . length
115
- ) } `;
146
+ return `${ pathStr } :${ node . getStartLineNumber ( ) } :${ char } \n ${ colorRange (
147
+ refFullLine ,
148
+ nodeOffset ,
149
+ nodeOffset + node . getText ( ) . length ,
150
+ nodeColor
151
+ ) } \n ${ ' ' . repeat ( nodeOffset ) } ${ chalk [ nodeColor ] ( '^' ) . repeat ( node . getText ( ) . length ) } `;
116
152
}
117
153
118
154
function getAliases ( node : ts . Node , methodBody : ts . Node ) {
@@ -245,7 +281,11 @@ function checkArrayFunctions(methodBody: ts.Node, pathStr: string) {
245
281
const arg = c . getArguments ( ) [ 0 ] ;
246
282
if ( isArrayOrObject ( arg ) && ! arg . getText ( ) . startsWith ( 'clone(' ) ) {
247
283
const msg = `Mutable objects must be cloned via "clone" before being pushed into an array` ;
248
- throw Error ( `${ msg } \n${ getNodeLines ( arg , pathStr ) } Suggestion: clone(${ arg . getText ( ) } )` ) ;
284
+ throw Error (
285
+ `${ msg } \n${ getNodeLines ( arg , pathStr , 'yellow' ) } ${ chalk . bold . underline . yellow (
286
+ 'Suggestion'
287
+ ) } : clone(${ arg . getText ( ) } )`
288
+ ) ;
249
289
}
250
290
}
251
291
}
0 commit comments