From 361e878539dd084f434581b73b1b725c1c54d574 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 16 Jun 2020 15:14:04 +0100 Subject: [PATCH] update "invalid helper key" error conditions and messages --- package-lock.json | 6 ++ package.json | 1 + src/lsp/providers/diagnosticsProvider.ts | 94 ++++++++++++++++++++---- src/lsp/util/stringToPath.ts | 15 ++++ 4 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 src/lsp/util/stringToPath.ts diff --git a/package-lock.json b/package-lock.json index 91c1aca..06b59a2 100755 --- a/package-lock.json +++ b/package-lock.json @@ -4591,6 +4591,12 @@ } } }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index a163469..2386c68 100755 --- a/package.json +++ b/package.json @@ -173,6 +173,7 @@ "globrex": "^0.1.2", "import-from": "^3.0.0", "jest": "^25.5.4", + "js-levenshtein": "^1.1.6", "line-column": "^1.0.2", "mitt": "^1.2.0", "mkdirp": "^1.0.3", diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts index 79f462e..2a59c26 100644 --- a/src/lsp/providers/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnosticsProvider.ts @@ -15,12 +15,15 @@ import { } from '../util/find' import { getClassNameMeta } from '../util/getClassNameMeta' import { getClassNameDecls } from '../util/getClassNameDecls' -import { equal, flatten } from '../../util/array' +import { equal } from '../../util/array' import { getDocumentSettings } from '../util/getDocumentSettings' const dlv = require('dlv') import semver from 'semver' import { getLanguageBoundaries } from '../util/getLanguageBoundaries' import { absoluteRange } from '../util/absoluteRange' +import { isObject } from '../../class-names/isObject' +import levenshtein from 'js-levenshtein' +import { stringToPath } from '../util/stringToPath' function getUnsupportedApplyDiagnostics( state: State, @@ -276,16 +279,84 @@ function getInvalidHelperKeyDiagnostics( matches.forEach((match) => { let base = match.groups.helper === 'theme' ? ['theme'] : [] - let keys = match.groups.key.split(/[.\[\]]/).filter(Boolean) + let keys = stringToPath(match.groups.key) let value = dlv(state.config, [...base, ...keys]) - if ( - typeof value === 'string' || - typeof value === 'number' || - value instanceof String || - value instanceof Number || - Array.isArray(value) - ) { + const isValid = (val: unknown): boolean => + typeof val === 'string' || + typeof val === 'number' || + val instanceof String || + val instanceof Number || + Array.isArray(val) + + const stitch = (keys: string[]): string => + keys.reduce((acc, cur, i) => { + if (i === 0) return cur + if (cur.includes('.')) return `${acc}[${cur}]` + return `${acc}.${cur}` + }, '') + + let message: string + + if (isValid(value)) { + // The value resolves successfully, but we need to check that there + // wasn't any funny business. If you have a theme object: + // { msg: 'hello' } and do theme('msg.0') + // this will resolve to 'h', which is probably not intentional, so we + // check that all of the keys are object or array keys (i.e. not string + // indexes) + let valid = true + for (let i = keys.length - 1; i >= 0; i--) { + let key = keys[i] + let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)]) + if (/^[0-9]+$/.test(key)) { + if (!isObject(parentValue) && !Array.isArray(parentValue)) { + valid = false + break + } + } else if (!isObject(parentValue)) { + valid = false + break + } + } + if (!valid) { + message = `'${match.groups.key}' does not exist in your theme config.` + } + } else if (typeof value === 'undefined') { + message = `'${match.groups.key}' does not exist in your theme config.` + let parentValue = dlv(state.config, [ + ...base, + ...keys.slice(0, keys.length - 1), + ]) + if (isObject(parentValue)) { + let validKeys = Object.keys(parentValue) + .filter((key) => isValid(parentValue[key])) + .sort( + (a, b) => + levenshtein(keys[keys.length - 1], a) - + levenshtein(keys[keys.length - 1], b) + ) + if (validKeys.length) { + message += ` Did you mean '${stitch([ + ...keys.slice(0, keys.length - 1), + validKeys[0], + ])}'?` + } + } + } else { + message = `'${match.groups.key}' was found but does not resolve to a string.` + + if (isObject(value)) { + let firstValidKey = Object.keys(value).find((key) => + isValid(value[key]) + ) + if (firstValidKey) { + message += ` Did you mean '${stitch([...keys, firstValidKey])}'?` + } + } + } + + if (!message) { return null } @@ -308,10 +379,7 @@ function getInvalidHelperKeyDiagnostics( severity === 'error' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, - message: - typeof value === 'undefined' - ? `'${match.groups.key}' does not exist in your theme config.` - : `'${match.groups.key}' was found but does not resolve to a string.`, + message, }) }) }) diff --git a/src/lsp/util/stringToPath.ts b/src/lsp/util/stringToPath.ts new file mode 100644 index 0000000..b06e153 --- /dev/null +++ b/src/lsp/util/stringToPath.ts @@ -0,0 +1,15 @@ +// https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L6735-L6744 +let rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g +let reEscapeChar = /\\(\\)?/g + +export function stringToPath(string: string): string[] { + let result: string[] = [] + if (string.charCodeAt(0) === 46 /* . */) { + result.push('') + } + // @ts-ignore + string.replace(rePropName, (match, number, quote, subString) => { + result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match) + }) + return result +}