tailwind-ctp-intellisense/src/lsp/providers/diagnosticsProvider.ts

408 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import {
TextDocument,
Diagnostic,
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, flatten } 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'
function getUnsupportedApplyDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.unsupportedApply
if (severity === 'ignore') return []
const classNames = findClassNamesInRange(document, undefined, 'css')
let diagnostics: Diagnostic[] = classNames
.map(({ className, range }) => {
const meta = getClassNameMeta(state, className)
if (!meta) return null
let message: string
if (Array.isArray(meta)) {
message = `\`@apply\` cannot be used with \`.${className}\` because it is included in multiple rulesets.`
} else if (meta.source !== 'utilities') {
message = `\`@apply\` cannot be used with \`.${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}\` because it is nested inside of an at-rule (${meta.context[0]}).`
} else {
message = `\`@apply\` cannot be used with \`.${className}\` because it is nested inside of at-rules (${meta.context.join(
', '
)}).`
}
} else if (meta.pseudo && meta.pseudo.length > 0) {
if (meta.pseudo.length === 1) {
message = `\`@apply\` cannot be used with \`.${className}\` because its definition includes a pseudo-selector (${meta.pseudo[0]})`
} else {
message = `\`@apply\` cannot be used with \`.${className}\` because its definition includes pseudo-selectors (${meta.pseudo.join(
', '
)})`
}
}
if (!message) return null
return {
severity:
severity === 'error'
? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning,
range,
message,
}
})
.filter(Boolean)
return diagnostics
}
function getUtilityConflictDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.utilityConflicts
if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = []
const classLists = findClassListsInDocument(state, document)
classLists.forEach((classList) => {
const classNames = getClassNamesInClassList(classList)
classNames.forEach((className, index) => {
let otherClassNames = classNames.filter((_className, i) => i !== index)
otherClassNames.forEach((otherClassName) => {
let decls = getClassNameDecls(state, className.className)
if (!decls) return
let otherDecls = getClassNameDecls(state, otherClassName.className)
if (!otherDecls) return
let meta = getClassNameMeta(state, className.className)
let otherMeta = getClassNameMeta(state, otherClassName.className)
if (
equal(Object.keys(decls), Object.keys(otherDecls)) &&
!Array.isArray(meta) &&
!Array.isArray(otherMeta) &&
equal(meta.context, otherMeta.context) &&
equal(meta.pseudo, otherMeta.pseudo)
) {
diagnostics.push({
range: className.range,
severity:
severity === 'error'
? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning,
message: `You cant use \`${className.className}\` and \`${otherClassName.className}\` together`,
relatedInformation: [
{
message: otherClassName.className,
location: {
uri: document.uri,
range: otherClassName.range,
},
},
],
})
}
})
})
})
return diagnostics
}
function getUnknownScreenDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.unknownScreen
if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = []
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
}
diagnostics.push({
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: 'Unknown screen',
})
})
})
return diagnostics
}
function getUnknownVariantDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.unknownVariant
if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = []
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 variantStartIndex =
listStartIndex + variants.slice(0, i).join('').length
diagnostics.push({
range: absoluteRange(
{
start: indexToPosition(text, variantStartIndex),
end: indexToPosition(text, variantStartIndex + variant.length),
},
range
),
severity:
severity === 'error'
? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning,
message: `Unknown variant: ${variant}`,
})
}
})
})
return diagnostics
}
function getUnknownConfigKeyDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.unknownConfigKey
if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = []
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 = match.groups.key.split(/[.\[\]]/).filter(Boolean)
let value = dlv(state.config, [...base, ...keys])
if (
typeof value === 'string' ||
typeof value === 'number' ||
value instanceof String ||
value instanceof Number ||
Array.isArray(value)
) {
return null
}
let startIndex =
match.index +
match.groups.prefix.length +
match.groups.helper.length +
1 + // open paren
match.groups.quote.length
diagnostics.push({
range: absoluteRange(
{
start: indexToPosition(text, startIndex),
end: indexToPosition(text, startIndex + match.groups.key.length),
},
range
),
severity:
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.`,
})
})
})
return diagnostics
}
function getUnsupportedTailwindDirectiveDiagnostics(
state: State,
document: TextDocument,
settings: Settings
): Diagnostic[] {
let severity = settings.lint.unsupportedTailwindDirective
if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = []
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
}
diagnostics.push({
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: `Unsupported value: ${match.groups.value}${
match.groups.value === 'preflight' ? '. Use base instead.' : ''
}`,
})
})
})
return diagnostics
}
export async function provideDiagnostics(
state: State,
document: TextDocument
): Promise<void> {
const settings = await getDocumentSettings(state, document)
const diagnostics: Diagnostic[] = settings.validate
? [
...getUtilityConflictDiagnostics(state, document, settings),
...getUnsupportedApplyDiagnostics(state, document, settings),
...getUnknownScreenDiagnostics(state, document, settings),
...getUnknownVariantDiagnostics(state, document, settings),
...getUnknownConfigKeyDiagnostics(state, document, settings),
...getUnsupportedTailwindDirectiveDiagnostics(
state,
document,
settings
),
]
: []
state.editor.connection.sendDiagnostics({
uri: document.uri,
diagnostics,
})
}