tidy up
parent
743f10ac3d
commit
a0c6229688
|
@ -33,6 +33,7 @@ import { joinWithAnd } from '../../util/joinWithAnd'
|
|||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
||||
import { isCssDoc } from '../../util/css'
|
||||
import { absoluteRange } from '../../util/absoluteRange'
|
||||
import type { NodeSource, Root } from 'postcss'
|
||||
|
||||
async function getDiagnosticsFromCodeActionParams(
|
||||
state: State,
|
||||
|
@ -201,6 +202,19 @@ async function provideUtilityConflictsCodeActions(
|
|||
]
|
||||
}
|
||||
|
||||
function postcssSourceToRange(source: NodeSource): Range {
|
||||
return {
|
||||
start: {
|
||||
line: source.start.line - 1,
|
||||
character: source.start.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: source.end.line - 1,
|
||||
character: source.end.column,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function provideInvalidApplyCodeActions(
|
||||
state: State,
|
||||
params: CodeActionParams,
|
||||
|
@ -211,7 +225,7 @@ async function provideInvalidApplyCodeActions(
|
|||
let cssRange: Range
|
||||
let cssText = documentText
|
||||
const { postcss } = state.modules
|
||||
let change: TextEdit
|
||||
let changes: TextEdit[] = []
|
||||
|
||||
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
|
||||
/\s+/
|
||||
|
@ -238,30 +252,18 @@ async function provideInvalidApplyCodeActions(
|
|||
try {
|
||||
await postcss([
|
||||
postcss.plugin('', (_options = {}) => {
|
||||
return (root) => {
|
||||
return (root: Root) => {
|
||||
root.walkRules((rule) => {
|
||||
if (change) return false
|
||||
if (changes.length) return false
|
||||
|
||||
rule.walkAtRules('apply', (atRule) => {
|
||||
let { start, end } = atRule.source
|
||||
let atRuleRange: Range = {
|
||||
start: {
|
||||
line: start.line - 1,
|
||||
character: start.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: end.line - 1,
|
||||
character: end.column - 1,
|
||||
},
|
||||
}
|
||||
let atRuleRange = postcssSourceToRange(atRule.source)
|
||||
if (cssRange) {
|
||||
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
||||
}
|
||||
|
||||
if (!isWithinRange(diagnostic.range.start, atRuleRange)) {
|
||||
// keep looking
|
||||
if (!isWithinRange(diagnostic.range.start, atRuleRange))
|
||||
return true
|
||||
}
|
||||
|
||||
let ast = classNameToAst(
|
||||
state,
|
||||
|
@ -270,35 +272,33 @@ async function provideInvalidApplyCodeActions(
|
|||
diagnostic.className.classList.important
|
||||
)
|
||||
|
||||
if (!ast) {
|
||||
return false
|
||||
}
|
||||
if (!ast) return false
|
||||
|
||||
rule.after(ast.nodes)
|
||||
let insertedRule = rule.next()
|
||||
if (!insertedRule) return false
|
||||
|
||||
if (totalClassNamesInClassList === 1) {
|
||||
atRule.remove()
|
||||
} else {
|
||||
changes.push({
|
||||
range: diagnostic.className.classList.range,
|
||||
newText: removeRangesFromString(
|
||||
diagnostic.className.classList.classList,
|
||||
diagnostic.className.relativeRange
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
let ruleRange = postcssSourceToRange(rule.source)
|
||||
if (cssRange) {
|
||||
ruleRange = absoluteRange(ruleRange, cssRange)
|
||||
}
|
||||
|
||||
let outputIndent: string
|
||||
let documentIndent = detectIndent(documentText)
|
||||
|
||||
let ruleRange: Range = {
|
||||
start: {
|
||||
line: rule.source.start.line - 1,
|
||||
character: rule.source.start.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: rule.source.end.line - 1,
|
||||
character: rule.source.end.column,
|
||||
},
|
||||
}
|
||||
if (cssRange) {
|
||||
ruleRange = absoluteRange(ruleRange, cssRange)
|
||||
}
|
||||
|
||||
change = {
|
||||
changes.push({
|
||||
range: ruleRange,
|
||||
newText:
|
||||
rule.toString() +
|
||||
|
@ -315,7 +315,7 @@ async function provideInvalidApplyCodeActions(
|
|||
documentIndent.indent
|
||||
)
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
||||
|
@ -327,7 +327,7 @@ async function provideInvalidApplyCodeActions(
|
|||
return []
|
||||
}
|
||||
|
||||
if (!change) {
|
||||
if (!changes.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -338,20 +338,7 @@ async function provideInvalidApplyCodeActions(
|
|||
diagnostics: [diagnostic],
|
||||
edit: {
|
||||
changes: {
|
||||
[params.textDocument.uri]: [
|
||||
...(totalClassNamesInClassList > 1
|
||||
? [
|
||||
{
|
||||
range: diagnostic.className.classList.range,
|
||||
newText: removeRangesFromString(
|
||||
diagnostic.className.classList.classList,
|
||||
diagnostic.className.relativeRange
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
change,
|
||||
],
|
||||
[params.textDocument.uri]: changes,
|
||||
},
|
||||
},
|
||||
},
|
|
@ -1,516 +1,13 @@
|
|||
import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
|
||||
import { State, Settings } from '../../util/state'
|
||||
import { isCssDoc } from '../../util/css'
|
||||
import {
|
||||
findClassNamesInRange,
|
||||
findClassListsInDocument,
|
||||
getClassNamesInClassList,
|
||||
findAll,
|
||||
indexToPosition,
|
||||
} from '../../util/find'
|
||||
import { getClassNameMeta } from '../../util/getClassNameMeta'
|
||||
import { getClassNameDecls } from '../../util/getClassNameDecls'
|
||||
import { equal } from '../../../util/array'
|
||||
import { TextDocument } from 'vscode-languageserver'
|
||||
import { State } from '../../util/state'
|
||||
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 { stringToPath } from '../../util/stringToPath'
|
||||
import { closest } from '../../util/closest'
|
||||
import {
|
||||
InvalidApplyDiagnostic,
|
||||
DiagnosticKind,
|
||||
UtilityConflictsDiagnostic,
|
||||
InvalidScreenDiagnostic,
|
||||
InvalidVariantDiagnostic,
|
||||
InvalidConfigPathDiagnostic,
|
||||
InvalidTailwindDirectiveDiagnostic,
|
||||
AugmentedDiagnostic,
|
||||
} from './types'
|
||||
import { joinWithAnd } from '../../util/joinWithAnd'
|
||||
|
||||
function getInvalidApplyDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidApplyDiagnostic[] {
|
||||
let severity = settings.lint.invalidApply
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
const classNames = findClassNamesInRange(document, undefined, 'css')
|
||||
|
||||
let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
|
||||
const meta = getClassNameMeta(state, className.className)
|
||||
if (!meta) return null
|
||||
|
||||
let message: string
|
||||
|
||||
if (Array.isArray(meta)) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is included in multiple rulesets.`
|
||||
} else if (meta.source !== 'utilities') {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is not a utility.`
|
||||
} else if (meta.context && meta.context.length > 0) {
|
||||
if (meta.context.length === 1) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
|
||||
} else {
|
||||
message = `'@apply' cannot be used with '${
|
||||
className.className
|
||||
}' because it is nested inside of at-rules (${meta.context
|
||||
.map((c) => `'${c}'`)
|
||||
.join(', ')}).`
|
||||
}
|
||||
} else if (meta.pseudo && meta.pseudo.length > 0) {
|
||||
if (meta.pseudo.length === 1) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
|
||||
} else {
|
||||
message = `'@apply' cannot be used with '${
|
||||
className.className
|
||||
}' because its definition includes pseudo-selectors (${meta.pseudo
|
||||
.map((p) => `'${p}'`)
|
||||
.join(', ')}).`
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) return null
|
||||
|
||||
return {
|
||||
code: DiagnosticKind.InvalidApply,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
range: className.range,
|
||||
message,
|
||||
className,
|
||||
}
|
||||
})
|
||||
|
||||
return diagnostics.filter(Boolean)
|
||||
}
|
||||
|
||||
function getUtilityConflictDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): UtilityConflictsDiagnostic[] {
|
||||
let severity = settings.lint.utilityConflicts
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: UtilityConflictsDiagnostic[] = []
|
||||
const classLists = findClassListsInDocument(state, document)
|
||||
|
||||
classLists.forEach((classList) => {
|
||||
const classNames = getClassNamesInClassList(classList)
|
||||
|
||||
classNames.forEach((className, index) => {
|
||||
let decls = getClassNameDecls(state, className.className)
|
||||
if (!decls) return
|
||||
|
||||
let properties = Object.keys(decls)
|
||||
let meta = getClassNameMeta(state, className.className)
|
||||
|
||||
let otherClassNames = classNames.filter((_className, i) => i !== index)
|
||||
|
||||
let conflictingClassNames = otherClassNames.filter((otherClassName) => {
|
||||
let otherDecls = getClassNameDecls(state, otherClassName.className)
|
||||
if (!otherDecls) return false
|
||||
|
||||
let otherMeta = getClassNameMeta(state, otherClassName.className)
|
||||
|
||||
return (
|
||||
equal(properties, Object.keys(otherDecls)) &&
|
||||
!Array.isArray(meta) &&
|
||||
!Array.isArray(otherMeta) &&
|
||||
equal(meta.context, otherMeta.context) &&
|
||||
equal(meta.pseudo, otherMeta.pseudo)
|
||||
)
|
||||
})
|
||||
|
||||
if (conflictingClassNames.length === 0) return
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.UtilityConflicts,
|
||||
className,
|
||||
otherClassNames: conflictingClassNames,
|
||||
range: className.range,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message: `'${className.className}' applies the same CSS ${
|
||||
properties.length === 1 ? 'property' : 'properties'
|
||||
} as ${joinWithAnd(
|
||||
conflictingClassNames.map(
|
||||
(conflictingClassName) => `'${conflictingClassName.className}'`
|
||||
)
|
||||
)}.`,
|
||||
relatedInformation: conflictingClassNames.map(
|
||||
(conflictingClassName) => {
|
||||
return {
|
||||
message: conflictingClassName.className,
|
||||
location: {
|
||||
uri: document.uri,
|
||||
range: conflictingClassName.range,
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
function getInvalidScreenDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidScreenDiagnostic[] {
|
||||
let severity = settings.lint.invalidScreen
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidScreenDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text)
|
||||
|
||||
let screens = Object.keys(
|
||||
dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
|
||||
)
|
||||
|
||||
matches.forEach((match) => {
|
||||
if (screens.includes(match.groups.screen)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message = `The screen '${match.groups.screen}' does not exist in your theme config.`
|
||||
let suggestions: string[] = []
|
||||
let suggestion = closest(match.groups.screen, screens)
|
||||
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidScreen,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - match.groups.screen.length
|
||||
),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
function getInvalidVariantDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidVariantDiagnostic[] {
|
||||
let severity = settings.lint.invalidVariant
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidVariantDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
|
||||
|
||||
matches.forEach((match) => {
|
||||
let variants = match.groups.variants.split(/(\s*,\s*)/)
|
||||
let listStartIndex =
|
||||
match.index + match[0].length - match.groups.variants.length
|
||||
|
||||
for (let i = 0; i < variants.length; i += 2) {
|
||||
let variant = variants[i].trim()
|
||||
if (state.variants.includes(variant)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let message = `The variant '${variant}' does not exist.`
|
||||
let suggestions: string[] = []
|
||||
let suggestion = closest(variant, state.variants)
|
||||
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
|
||||
let variantStartIndex =
|
||||
listStartIndex + variants.slice(0, i).join('').length
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidVariant,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(text, variantStartIndex),
|
||||
end: indexToPosition(text, variantStartIndex + variant.length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
function getInvalidConfigPathDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidConfigPathDiagnostic[] {
|
||||
let severity = settings.lint.invalidConfigPath
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidConfigPathDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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 keys = stringToPath(match.groups.key)
|
||||
let value = dlv(state.config, [...base, ...keys])
|
||||
|
||||
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
|
||||
let suggestions: 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 closestValidKey = closest(
|
||||
keys[keys.length - 1],
|
||||
Object.keys(parentValue).filter((key) => isValid(parentValue[key]))
|
||||
)
|
||||
if (closestValidKey) {
|
||||
suggestions.push(
|
||||
stitch([...keys.slice(0, keys.length - 1), closestValidKey])
|
||||
)
|
||||
message += ` Did you mean '${suggestions[0]}'?`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message = `'${match.groups.key}' was found but does not resolve to a string.`
|
||||
|
||||
if (isObject(value)) {
|
||||
let validKeys = Object.keys(value).filter((key) =>
|
||||
isValid(value[key])
|
||||
)
|
||||
if (validKeys.length) {
|
||||
suggestions.push(
|
||||
...validKeys.map((validKey) => stitch([...keys, validKey]))
|
||||
)
|
||||
message += ` Did you mean something like '${suggestions[0]}'?`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return null
|
||||
}
|
||||
|
||||
let startIndex =
|
||||
match.index +
|
||||
match.groups.prefix.length +
|
||||
match.groups.helper.length +
|
||||
1 + // open paren
|
||||
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'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
function getInvalidTailwindDirectiveDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidTailwindDirectiveDiagnostic[] {
|
||||
let severity = settings.lint.invalidTailwindDirective
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text)
|
||||
|
||||
let valid = [
|
||||
'utilities',
|
||||
'components',
|
||||
'screens',
|
||||
semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
|
||||
]
|
||||
|
||||
matches.forEach((match) => {
|
||||
if (valid.includes(match.groups.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message = `'${match.groups.value}' is not a valid group.`
|
||||
let suggestions: string[] = []
|
||||
|
||||
if (match.groups.value === 'preflight') {
|
||||
suggestions.push('base')
|
||||
message += ` Did you mean 'base'?`
|
||||
} else {
|
||||
let suggestion = closest(match.groups.value, valid)
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidTailwindDirective,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - match.groups.value.length
|
||||
),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
import { DiagnosticKind, AugmentedDiagnostic } from './types'
|
||||
import { getUtilityConflictDiagnostics } from './getUtilityConflictDiagnostics'
|
||||
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
|
||||
import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
|
||||
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
|
||||
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
|
||||
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
|
||||
|
||||
export async function getDiagnostics(
|
||||
state: State,
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { findClassNamesInRange } from '../../util/find'
|
||||
import { InvalidApplyDiagnostic, DiagnosticKind } from './types'
|
||||
import { Settings, State } from '../../util/state'
|
||||
import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { getClassNameMeta } from '../../util/getClassNameMeta'
|
||||
|
||||
export function getInvalidApplyDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidApplyDiagnostic[] {
|
||||
let severity = settings.lint.invalidApply
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
const classNames = findClassNamesInRange(document, undefined, 'css')
|
||||
|
||||
let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
|
||||
const meta = getClassNameMeta(state, className.className)
|
||||
if (!meta) return null
|
||||
|
||||
let message: string
|
||||
|
||||
if (Array.isArray(meta)) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is included in multiple rulesets.`
|
||||
} else if (meta.source !== 'utilities') {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is not a utility.`
|
||||
} else if (meta.context && meta.context.length > 0) {
|
||||
if (meta.context.length === 1) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
|
||||
} else {
|
||||
message = `'@apply' cannot be used with '${
|
||||
className.className
|
||||
}' because it is nested inside of at-rules (${meta.context
|
||||
.map((c) => `'${c}'`)
|
||||
.join(', ')}).`
|
||||
}
|
||||
} else if (meta.pseudo && meta.pseudo.length > 0) {
|
||||
if (meta.pseudo.length === 1) {
|
||||
message = `'@apply' cannot be used with '${className.className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
|
||||
} else {
|
||||
message = `'@apply' cannot be used with '${
|
||||
className.className
|
||||
}' because its definition includes pseudo-selectors (${meta.pseudo
|
||||
.map((p) => `'${p}'`)
|
||||
.join(', ')}).`
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) return null
|
||||
|
||||
return {
|
||||
code: DiagnosticKind.InvalidApply,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
range: className.range,
|
||||
message,
|
||||
className,
|
||||
}
|
||||
})
|
||||
|
||||
return diagnostics.filter(Boolean)
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
import { State, Settings } from '../../util/state'
|
||||
import { 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'
|
||||
const dlv = require('dlv')
|
||||
|
||||
export function getInvalidConfigPathDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidConfigPathDiagnostic[] {
|
||||
let severity = settings.lint.invalidConfigPath
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidConfigPathDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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 keys = stringToPath(match.groups.key)
|
||||
let value = dlv(state.config, [...base, ...keys])
|
||||
|
||||
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
|
||||
let suggestions: 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 closestValidKey = closest(
|
||||
keys[keys.length - 1],
|
||||
Object.keys(parentValue).filter((key) => isValid(parentValue[key]))
|
||||
)
|
||||
if (closestValidKey) {
|
||||
suggestions.push(
|
||||
stitch([...keys.slice(0, keys.length - 1), closestValidKey])
|
||||
)
|
||||
message += ` Did you mean '${suggestions[0]}'?`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message = `'${match.groups.key}' was found but does not resolve to a string.`
|
||||
|
||||
if (isObject(value)) {
|
||||
let validKeys = Object.keys(value).filter((key) =>
|
||||
isValid(value[key])
|
||||
)
|
||||
if (validKeys.length) {
|
||||
suggestions.push(
|
||||
...validKeys.map((validKey) => stitch([...keys, validKey]))
|
||||
)
|
||||
message += ` Did you mean something like '${suggestions[0]}'?`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return null
|
||||
}
|
||||
|
||||
let startIndex =
|
||||
match.index +
|
||||
match.groups.prefix.length +
|
||||
match.groups.helper.length +
|
||||
1 + // open paren
|
||||
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'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { State, Settings } from '../../util/state'
|
||||
import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { InvalidScreenDiagnostic, DiagnosticKind } from './types'
|
||||
import { isCssDoc } from '../../util/css'
|
||||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
||||
import { findAll, indexToPosition } from '../../util/find'
|
||||
import { closest } from '../../util/closest'
|
||||
import { absoluteRange } from '../../util/absoluteRange'
|
||||
const dlv = require('dlv')
|
||||
|
||||
export function getInvalidScreenDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidScreenDiagnostic[] {
|
||||
let severity = settings.lint.invalidScreen
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidScreenDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text)
|
||||
|
||||
let screens = Object.keys(
|
||||
dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
|
||||
)
|
||||
|
||||
matches.forEach((match) => {
|
||||
if (screens.includes(match.groups.screen)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message = `The screen '${match.groups.screen}' does not exist in your theme config.`
|
||||
let suggestions: string[] = []
|
||||
let suggestion = closest(match.groups.screen, screens)
|
||||
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidScreen,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - match.groups.screen.length
|
||||
),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import { State, Settings } from '../../util/state'
|
||||
import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { InvalidTailwindDirectiveDiagnostic, DiagnosticKind } from './types'
|
||||
import { isCssDoc } from '../../util/css'
|
||||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
||||
import { findAll, indexToPosition } from '../../util/find'
|
||||
import semver from 'semver'
|
||||
import { closest } from '../../util/closest'
|
||||
import { absoluteRange } from '../../util/absoluteRange'
|
||||
|
||||
export function getInvalidTailwindDirectiveDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidTailwindDirectiveDiagnostic[] {
|
||||
let severity = settings.lint.invalidTailwindDirective
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text)
|
||||
|
||||
let valid = [
|
||||
'utilities',
|
||||
'components',
|
||||
'screens',
|
||||
semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
|
||||
]
|
||||
|
||||
matches.forEach((match) => {
|
||||
if (valid.includes(match.groups.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message = `'${match.groups.value}' is not a valid group.`
|
||||
let suggestions: string[] = []
|
||||
|
||||
if (match.groups.value === 'preflight') {
|
||||
suggestions.push('base')
|
||||
message += ` Did you mean 'base'?`
|
||||
} else {
|
||||
let suggestion = closest(match.groups.value, valid)
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidTailwindDirective,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - match.groups.value.length
|
||||
),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { State, Settings } from '../../util/state'
|
||||
import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { InvalidVariantDiagnostic, DiagnosticKind } from './types'
|
||||
import { isCssDoc } from '../../util/css'
|
||||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
||||
import { findAll, indexToPosition } from '../../util/find'
|
||||
import { closest } from '../../util/closest'
|
||||
import { absoluteRange } from '../../util/absoluteRange'
|
||||
|
||||
export function getInvalidVariantDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): InvalidVariantDiagnostic[] {
|
||||
let severity = settings.lint.invalidVariant
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidVariantDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
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(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
|
||||
|
||||
matches.forEach((match) => {
|
||||
let variants = match.groups.variants.split(/(\s*,\s*)/)
|
||||
let listStartIndex =
|
||||
match.index + match[0].length - match.groups.variants.length
|
||||
|
||||
for (let i = 0; i < variants.length; i += 2) {
|
||||
let variant = variants[i].trim()
|
||||
if (state.variants.includes(variant)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let message = `The variant '${variant}' does not exist.`
|
||||
let suggestions: string[] = []
|
||||
let suggestion = closest(variant, state.variants)
|
||||
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
message += ` Did you mean '${suggestion}'?`
|
||||
}
|
||||
|
||||
let variantStartIndex =
|
||||
listStartIndex + variants.slice(0, i).join('').length
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidVariant,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(text, variantStartIndex),
|
||||
end: indexToPosition(text, variantStartIndex + variant.length),
|
||||
},
|
||||
range
|
||||
),
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message,
|
||||
suggestions,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { joinWithAnd } from '../../util/joinWithAnd'
|
||||
import { State, Settings } from '../../util/state'
|
||||
import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { UtilityConflictsDiagnostic, DiagnosticKind } from './types'
|
||||
import {
|
||||
findClassListsInDocument,
|
||||
getClassNamesInClassList,
|
||||
} from '../../util/find'
|
||||
import { getClassNameDecls } from '../../util/getClassNameDecls'
|
||||
import { getClassNameMeta } from '../../util/getClassNameMeta'
|
||||
import { equal } from '../../../util/array'
|
||||
|
||||
export function getUtilityConflictDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): UtilityConflictsDiagnostic[] {
|
||||
let severity = settings.lint.utilityConflicts
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: UtilityConflictsDiagnostic[] = []
|
||||
const classLists = findClassListsInDocument(state, document)
|
||||
|
||||
classLists.forEach((classList) => {
|
||||
const classNames = getClassNamesInClassList(classList)
|
||||
|
||||
classNames.forEach((className, index) => {
|
||||
let decls = getClassNameDecls(state, className.className)
|
||||
if (!decls) return
|
||||
|
||||
let properties = Object.keys(decls)
|
||||
let meta = getClassNameMeta(state, className.className)
|
||||
|
||||
let otherClassNames = classNames.filter((_className, i) => i !== index)
|
||||
|
||||
let conflictingClassNames = otherClassNames.filter((otherClassName) => {
|
||||
let otherDecls = getClassNameDecls(state, otherClassName.className)
|
||||
if (!otherDecls) return false
|
||||
|
||||
let otherMeta = getClassNameMeta(state, otherClassName.className)
|
||||
|
||||
return (
|
||||
equal(properties, Object.keys(otherDecls)) &&
|
||||
!Array.isArray(meta) &&
|
||||
!Array.isArray(otherMeta) &&
|
||||
equal(meta.context, otherMeta.context) &&
|
||||
equal(meta.pseudo, otherMeta.pseudo)
|
||||
)
|
||||
})
|
||||
|
||||
if (conflictingClassNames.length === 0) return
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.UtilityConflicts,
|
||||
className,
|
||||
otherClassNames: conflictingClassNames,
|
||||
range: className.range,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? DiagnosticSeverity.Error
|
||||
: DiagnosticSeverity.Warning,
|
||||
message: `'${className.className}' applies the same CSS ${
|
||||
properties.length === 1 ? 'property' : 'properties'
|
||||
} as ${joinWithAnd(
|
||||
conflictingClassNames.map(
|
||||
(conflictingClassName) => `'${conflictingClassName.className}'`
|
||||
)
|
||||
)}.`,
|
||||
relatedInformation: conflictingClassNames.map(
|
||||
(conflictingClassName) => {
|
||||
return {
|
||||
message: conflictingClassName.className,
|
||||
location: {
|
||||
uri: document.uri,
|
||||
range: conflictingClassName.range,
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -34,7 +34,7 @@ import {
|
|||
clearAllDiagnostics,
|
||||
} from './providers/diagnostics/diagnosticsProvider'
|
||||
import { createEmitter } from '../lib/emitter'
|
||||
import { provideCodeActions } from './providers/codeActionProvider'
|
||||
import { provideCodeActions } from './providers/codeActions/codeActionProvider'
|
||||
|
||||
let connection = createConnection(ProposedFeatures.all)
|
||||
let state: State = { enabled: false, emitter: createEmitter(connection) }
|
||||
|
|
Loading…
Reference in New Issue