master
Brad Cornes 2020-06-19 11:52:38 +01:00
parent 743f10ac3d
commit a0c6229688
9 changed files with 582 additions and 564 deletions

View File

@ -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,
},
},
},

View File

@ -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,

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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) }