@@ -8,8 +8,164 @@ import { stringToPath } from '../../util/stringToPath'
8
8
import isObject from '../../../util/isObject'
9
9
import { closest } from '../../util/closest'
10
10
import { absoluteRange } from '../../util/absoluteRange'
11
+ import { combinations } from '../../util/combinations'
11
12
const dlv = require ( 'dlv' )
12
13
14
+ function pathToString ( path : string | string [ ] ) : string {
15
+ if ( typeof path === 'string' ) return path
16
+ return path . reduce ( ( acc , cur , i ) => {
17
+ if ( i === 0 ) return cur
18
+ if ( cur . includes ( '.' ) ) return `${ acc } [${ cur } ]`
19
+ return `${ acc } .${ cur } `
20
+ } , '' )
21
+ }
22
+
23
+ function validateConfigPath (
24
+ state : State ,
25
+ path : string | string [ ] ,
26
+ base : string [ ] = [ ]
27
+ ) :
28
+ | { isValid : true ; value : any }
29
+ | { isValid : false ; reason : string ; suggestions : string [ ] } {
30
+ let keys = Array . isArray ( path ) ? path : stringToPath ( path )
31
+ let value = dlv ( state . config , [ ...base , ...keys ] )
32
+ let suggestions : string [ ] = [ ]
33
+
34
+ function findAlternativePath ( ) : string [ ] {
35
+ let points = combinations ( '123456789' . substr ( 0 , keys . length - 1 ) ) . map ( ( x ) =>
36
+ x . split ( '' ) . map ( ( x ) => parseInt ( x , 10 ) )
37
+ )
38
+
39
+ let possibilities : string [ ] [ ] = points
40
+ . map ( ( p ) => {
41
+ let result = [ ]
42
+ let i = 0
43
+ p . forEach ( ( x ) => {
44
+ result . push ( keys . slice ( i , x ) . join ( '.' ) )
45
+ i = x
46
+ } )
47
+ result . push ( keys . slice ( i ) . join ( '.' ) )
48
+ return result
49
+ } )
50
+ . slice ( 1 ) // skip original path
51
+
52
+ return possibilities . find (
53
+ ( possibility ) => validateConfigPath ( state , possibility , base ) . isValid
54
+ )
55
+ }
56
+
57
+ if ( typeof value === 'undefined' ) {
58
+ let reason = `'${ pathToString ( path ) } ' does not exist in your theme config.`
59
+ let parentPath = [ ...base , ...keys . slice ( 0 , keys . length - 1 ) ]
60
+ let parentValue = dlv ( state . config , parentPath )
61
+
62
+ if ( isObject ( parentValue ) ) {
63
+ let closestValidKey = closest (
64
+ keys [ keys . length - 1 ] ,
65
+ Object . keys ( parentValue ) . filter (
66
+ ( key ) => validateConfigPath ( state , [ ...parentPath , key ] ) . isValid
67
+ )
68
+ )
69
+ if ( closestValidKey ) {
70
+ suggestions . push (
71
+ pathToString ( [ ...keys . slice ( 0 , keys . length - 1 ) , closestValidKey ] )
72
+ )
73
+ reason += ` Did you mean '${ suggestions [ 0 ] } '?`
74
+ }
75
+ } else {
76
+ let altPath = findAlternativePath ( )
77
+ if ( altPath ) {
78
+ return {
79
+ isValid : false ,
80
+ reason : `${ reason } Did you mean '${ pathToString ( altPath ) } '?` ,
81
+ suggestions : [ pathToString ( altPath ) ] ,
82
+ }
83
+ }
84
+ }
85
+
86
+ return {
87
+ isValid : false ,
88
+ reason,
89
+ suggestions,
90
+ }
91
+ }
92
+
93
+ if (
94
+ ! (
95
+ typeof value === 'string' ||
96
+ typeof value === 'number' ||
97
+ value instanceof String ||
98
+ value instanceof Number ||
99
+ Array . isArray ( value )
100
+ )
101
+ ) {
102
+ let reason = `'${ pathToString (
103
+ path
104
+ ) } ' was found but does not resolve to a string.`
105
+
106
+ if ( isObject ( value ) ) {
107
+ let validKeys = Object . keys ( value ) . filter (
108
+ ( key ) => validateConfigPath ( state , [ ...keys , key ] , base ) . isValid
109
+ )
110
+ if ( validKeys . length ) {
111
+ suggestions . push (
112
+ ...validKeys . map ( ( validKey ) => pathToString ( [ ...keys , validKey ] ) )
113
+ )
114
+ reason += ` Did you mean something like '${ suggestions [ 0 ] } '?`
115
+ }
116
+ }
117
+ return {
118
+ isValid : false ,
119
+ reason,
120
+ suggestions,
121
+ }
122
+ }
123
+
124
+ // The value resolves successfully, but we need to check that there
125
+ // wasn't any funny business. If you have a theme object:
126
+ // { msg: 'hello' } and do theme('msg.0')
127
+ // this will resolve to 'h', which is probably not intentional, so we
128
+ // check that all of the keys are object or array keys (i.e. not string
129
+ // indexes)
130
+ let isValid = true
131
+ for ( let i = keys . length - 1 ; i >= 0 ; i -- ) {
132
+ let key = keys [ i ]
133
+ let parentValue = dlv ( state . config , [ ...base , ...keys . slice ( 0 , i ) ] )
134
+ if ( / ^ [ 0 - 9 ] + $ / . test ( key ) ) {
135
+ if ( ! isObject ( parentValue ) && ! Array . isArray ( parentValue ) ) {
136
+ isValid = false
137
+ break
138
+ }
139
+ } else if ( ! isObject ( parentValue ) ) {
140
+ isValid = false
141
+ break
142
+ }
143
+ }
144
+ if ( ! isValid ) {
145
+ let reason = `'${ pathToString ( path ) } ' does not exist in your theme config.`
146
+
147
+ let altPath = findAlternativePath ( )
148
+ if ( altPath ) {
149
+ return {
150
+ isValid : false ,
151
+ reason : `${ reason } Did you mean '${ pathToString ( altPath ) } '?` ,
152
+ suggestions : [ pathToString ( altPath ) ] ,
153
+ }
154
+ }
155
+
156
+ return {
157
+ isValid : false ,
158
+ reason,
159
+ suggestions : [ ] ,
160
+ }
161
+ }
162
+
163
+ return {
164
+ isValid : true ,
165
+ value,
166
+ }
167
+ }
168
+
13
169
export function getInvalidConfigPathDiagnostics (
14
170
state : State ,
15
171
document : TextDocument ,
@@ -38,85 +194,9 @@ export function getInvalidConfigPathDiagnostics(
38
194
39
195
matches . forEach ( ( match ) => {
40
196
let base = match . groups . helper === 'theme' ? [ 'theme' ] : [ ]
41
- let keys = stringToPath ( match . groups . key )
42
- let value = dlv ( state . config , [ ...base , ...keys ] )
43
-
44
- const isValid = ( val : unknown ) : boolean =>
45
- typeof val === 'string' ||
46
- typeof val === 'number' ||
47
- val instanceof String ||
48
- val instanceof Number ||
49
- Array . isArray ( val )
50
-
51
- const stitch = ( keys : string [ ] ) : string =>
52
- keys . reduce ( ( acc , cur , i ) => {
53
- if ( i === 0 ) return cur
54
- if ( cur . includes ( '.' ) ) return `${ acc } [${ cur } ]`
55
- return `${ acc } .${ cur } `
56
- } , '' )
57
-
58
- let message : string
59
- let suggestions : string [ ] = [ ]
60
-
61
- if ( isValid ( value ) ) {
62
- // The value resolves successfully, but we need to check that there
63
- // wasn't any funny business. If you have a theme object:
64
- // { msg: 'hello' } and do theme('msg.0')
65
- // this will resolve to 'h', which is probably not intentional, so we
66
- // check that all of the keys are object or array keys (i.e. not string
67
- // indexes)
68
- let valid = true
69
- for ( let i = keys . length - 1 ; i >= 0 ; i -- ) {
70
- let key = keys [ i ]
71
- let parentValue = dlv ( state . config , [ ...base , ...keys . slice ( 0 , i ) ] )
72
- if ( / ^ [ 0 - 9 ] + $ / . test ( key ) ) {
73
- if ( ! isObject ( parentValue ) && ! Array . isArray ( parentValue ) ) {
74
- valid = false
75
- break
76
- }
77
- } else if ( ! isObject ( parentValue ) ) {
78
- valid = false
79
- break
80
- }
81
- }
82
- if ( ! valid ) {
83
- message = `'${ match . groups . key } ' does not exist in your theme config.`
84
- }
85
- } else if ( typeof value === 'undefined' ) {
86
- message = `'${ match . groups . key } ' does not exist in your theme config.`
87
- let parentValue = dlv ( state . config , [
88
- ...base ,
89
- ...keys . slice ( 0 , keys . length - 1 ) ,
90
- ] )
91
- if ( isObject ( parentValue ) ) {
92
- let closestValidKey = closest (
93
- keys [ keys . length - 1 ] ,
94
- Object . keys ( parentValue ) . filter ( ( key ) => isValid ( parentValue [ key ] ) )
95
- )
96
- if ( closestValidKey ) {
97
- suggestions . push (
98
- stitch ( [ ...keys . slice ( 0 , keys . length - 1 ) , closestValidKey ] )
99
- )
100
- message += ` Did you mean '${ suggestions [ 0 ] } '?`
101
- }
102
- }
103
- } else {
104
- message = `'${ match . groups . key } ' was found but does not resolve to a string.`
105
-
106
- if ( isObject ( value ) ) {
107
- let validKeys = Object . keys ( value ) . filter ( ( key ) =>
108
- isValid ( value [ key ] )
109
- )
110
- if ( validKeys . length ) {
111
- suggestions . push (
112
- ...validKeys . map ( ( validKey ) => stitch ( [ ...keys , validKey ] ) )
113
- )
114
- message += ` Did you mean something like '${ suggestions [ 0 ] } '?`
115
- }
116
- }
117
- }
197
+ let result = validateConfigPath ( state , match . groups . key , base )
118
198
119
- if ( ! message ) {
199
+ if ( result . isValid === true ) {
120
200
return null
121
201
}
122
202
@@ -140,8 +220,8 @@ export function getInvalidConfigPathDiagnostics(
140
220
severity === 'error'
141
221
? DiagnosticSeverity . Error
142
222
: DiagnosticSeverity . Warning ,
143
- message,
144
- suggestions,
223
+ message : result . reason ,
224
+ suggestions : result . suggestions ,
145
225
} )
146
226
} )
147
227
} )
0 commit comments