@@ -8,8 +8,164 @@ import { stringToPath } from '../../util/stringToPath'
88import isObject from '../../../util/isObject'
99import { closest } from '../../util/closest'
1010import { absoluteRange } from '../../util/absoluteRange'
11+ import { combinations } from '../../util/combinations'
1112const dlv = require ( 'dlv' )
1213
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+
13169export function getInvalidConfigPathDiagnostics (
14170 state : State ,
15171 document : TextDocument ,
@@ -38,85 +194,9 @@ export function getInvalidConfigPathDiagnostics(
38194
39195 matches . forEach ( ( match ) => {
40196 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 )
118198
119- if ( ! message ) {
199+ if ( result . isValid === true ) {
120200 return null
121201 }
122202
@@ -140,8 +220,8 @@ export function getInvalidConfigPathDiagnostics(
140220 severity === 'error'
141221 ? DiagnosticSeverity . Error
142222 : DiagnosticSeverity . Warning ,
143- message,
144- suggestions,
223+ message : result . reason ,
224+ suggestions : result . suggestions ,
145225 } )
146226 } )
147227 } )
0 commit comments