import { State , Settings } from '../util/state' ; import type { TextDocument , Range , DiagnosticSeverity } from 'vscode-languageserver' ; import { InvalidConfigPathDiagnostic , DiagnosticKind } from './types' ; import { isCssDoc } from '../util/css' ; import { getLanguageBoundaries } from '../util/getLanguageBoundaries' ; import { findAll , indexToPosition } from '../util/find' ; import { stringToPath } from '../util/stringToPath' ; import isObject from '../util/isObject' ; import { closest } from '../util/closest' ; import { absoluteRange } from '../util/absoluteRange' ; import { combinations } from '../util/combinations' ; const dlv = require ( 'dlv' ) ; function pathToString ( path ) { if ( typeof path === 'string' ) return path ; return path . reduce ( ( acc , cur , i ) => { if ( i === 0 ) return cur ; if ( cur . includes ( '.' ) ) return `${ acc }[${ cur }]` ; return `${ acc }.${ cur }` ; } , '' ) ; } export function validateConfigPath ( state , path , base = [ ] ) { let keys = Array . isArray ( path ) ? path : stringToPath ( path ) ; let value = dlv ( state . config , [ ... base , ... keys ] ) ; let suggestions = [ ] ; function findAlternativePath ( ) { let points = combinations ( '123456789' . substr ( 0 , keys . length - 1 ) ) . map ( ( x ) => x . split ( '' ) . map ( ( x ) => parseInt ( x , 10 ) ) ) ; let possibilities = points . map ( ( p ) => { let result = [ ] ; let i = 0 ; p . forEach ( ( x ) => { result . push ( keys . slice ( i , x ) . join ( '.' ) ) ; i = x ; } ) ; result . push ( keys . slice ( i ) . join ( '.' ) ) ; return result ; } ) . slice ( 1 ) ; return possibilities . find ( ( possibility ) => validateConfigPath ( state , possibility , base ) . isValid ) ; } if ( typeof value === 'undefined' ) { let reason = `'${ pathToString ( path ) }' does not exist in your theme config.` ; let parentPath = [ ... base , ... keys . slice ( 0 , keys . length - 1 ) ] ; let parentValue = dlv ( state . config , parentPath ) ; if ( isObject ( parentValue ) ) { let closestValidKey = closest ( keys [ keys . length - 1 ] , Object . keys ( parentValue ) . filter ( ( key ) => validateConfigPath ( state , [ ... parentPath , key ] ) . isValid ) ) ; if ( closestValidKey ) { suggestions . push ( pathToString ( [ ... keys . slice ( 0 , keys . length - 1 ) , closestValidKey ] ) ) ; reason += ` Did you mean '${ suggestions [ 0 ] }'?` ; } } else { let altPath = findAlternativePath ( ) ; if ( altPath ) { return { isValid : false , reason : `${ reason } Did you mean '${ pathToString ( altPath ) }'?` , suggestions : [ pathToString ( altPath ) ] } ; } } return { isValid : false , reason , suggestions } ; } if ( ! ( typeof value === 'string' || typeof value === 'number' || value instanceof String || value instanceof Number || Array . isArray ( value ) || typeof value === 'function' ) ) { let reason = `'${ pathToString ( path ) }' was found but does not resolve to a valid theme value.` ; if ( isObject ( value ) ) { let validKeys = Object . keys ( value ) . filter ( ( key ) => validateConfigPath ( state , [ ... keys , key ] , base ) . isValid ) ; if ( validKeys . length ) { suggestions . push ( ... validKeys . map ( ( validKey ) => pathToString ( [ ... keys , validKey ] ) ) ) ; reason += ` Did you mean something like '${ suggestions [ 0 ] }'?` ; } } return { isValid : false , reason , suggestions } ; } let isValid = 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 ) ) { isValid = false ; break ; } } else if ( ! isObject ( parentValue ) ) { isValid = false ; break ; } } if ( ! isValid ) { let reason = `'${ pathToString ( path ) }' does not exist in your theme config.` ; let altPath = findAlternativePath ( ) ; if ( altPath ) { return { isValid : false , reason : `${ reason } Did you mean '${ pathToString ( altPath ) }'?` , suggestions : [ pathToString ( altPath ) ] } ; } return { isValid : false , reason , suggestions : [ ] } ; } return { isValid : true , value } ; } export function getInvalidConfigPathDiagnostics ( state , document , settings ) { let severity = settings . tailwindCSS . lint . invalidConfigPath ; if ( severity === 'ignore' ) return [ ] ; let diagnostics = [ ] ; let ranges = [ ] ; if ( isCssDoc ( state , document ) ) { ranges . push ( undefined ) ; } else { let boundaries = getLanguageBoundaries ( state , document ) ; if ( ! boundaries ) return [ ] ; ranges . push ( ... boundaries . css ) ; } ranges . forEach ( ( range ) => { let text = document . getText ( range ) ; let matches = findAll ( /(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/g , text ) ; matches . forEach ( ( match ) => { let base = match . groups . helper === 'theme' ? [ 'theme' ] : [ ] ; let result = validateConfigPath ( state , match . groups . key , base ) ; if ( result . isValid === true ) { return null ; } let startIndex = match . index + match . groups . prefix . length + match . groups . helper . length + 1 + match . groups . quote . length ; diagnostics . push ( { code : DiagnosticKind . InvalidConfigPath , range : absoluteRange ( { start : indexToPosition ( text , startIndex ) , end : indexToPosition ( text , startIndex + match . groups . key . length ) } , range ) , severity : severity === 'error' ? 1 : 2 , message : result . reason , suggestions : result . suggestions } ) ; } ) ; } ) ; return diagnostics ; }