Skip to content

Commit 2df8544

Browse files
committed
refactor invalid config path diagnostics
1 parent 03e16a4 commit 2df8544

File tree

3 files changed

+174
-94
lines changed

3 files changed

+174
-94
lines changed

src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts

Lines changed: 160 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,164 @@ import { stringToPath } from '../../util/stringToPath'
88
import isObject from '../../../util/isObject'
99
import { closest } from '../../util/closest'
1010
import { absoluteRange } from '../../util/absoluteRange'
11+
import { combinations } from '../../util/combinations'
1112
const 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+
13169
export 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
})

src/lsp/util/combinations.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function combinations(str: string): string[] {
2+
let fn = function (active: string, rest: string, a: string[]) {
3+
if (!active && !rest) return
4+
if (!rest) {
5+
a.push(active)
6+
} else {
7+
fn(active + rest[0], rest.slice(1), a)
8+
fn(active, rest.slice(1), a)
9+
}
10+
return a
11+
}
12+
return fn('', str, [])
13+
}

src/lsp/util/getClassNameAtPosition.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { State } from './state'
2+
import { combinations } from './combinations'
23
const dlv = require('dlv')
34

45
export function getClassNameParts(state: State, className: string): string[] {
@@ -41,17 +42,3 @@ export function getClassNameParts(state: State, className: string): string[] {
4142
return false
4243
})
4344
}
44-
45-
function combinations(str: string): string[] {
46-
let fn = function (active: string, rest: string, a: string[]) {
47-
if (!active && !rest) return
48-
if (!rest) {
49-
a.push(active)
50-
} else {
51-
fn(active + rest[0], rest.slice(1), a)
52-
fn(active, rest.slice(1), a)
53-
}
54-
return a
55-
}
56-
return fn('', str, [])
57-
}

0 commit comments

Comments
 (0)