tidy up
parent
743f10ac3d
commit
a0c6229688
|
@ -33,6 +33,7 @@ import { joinWithAnd } from '../../util/joinWithAnd'
|
||||||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
||||||
import { isCssDoc } from '../../util/css'
|
import { isCssDoc } from '../../util/css'
|
||||||
import { absoluteRange } from '../../util/absoluteRange'
|
import { absoluteRange } from '../../util/absoluteRange'
|
||||||
|
import type { NodeSource, Root } from 'postcss'
|
||||||
|
|
||||||
async function getDiagnosticsFromCodeActionParams(
|
async function getDiagnosticsFromCodeActionParams(
|
||||||
state: State,
|
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(
|
async function provideInvalidApplyCodeActions(
|
||||||
state: State,
|
state: State,
|
||||||
params: CodeActionParams,
|
params: CodeActionParams,
|
||||||
|
@ -211,7 +225,7 @@ async function provideInvalidApplyCodeActions(
|
||||||
let cssRange: Range
|
let cssRange: Range
|
||||||
let cssText = documentText
|
let cssText = documentText
|
||||||
const { postcss } = state.modules
|
const { postcss } = state.modules
|
||||||
let change: TextEdit
|
let changes: TextEdit[] = []
|
||||||
|
|
||||||
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
|
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
|
||||||
/\s+/
|
/\s+/
|
||||||
|
@ -238,30 +252,18 @@ async function provideInvalidApplyCodeActions(
|
||||||
try {
|
try {
|
||||||
await postcss([
|
await postcss([
|
||||||
postcss.plugin('', (_options = {}) => {
|
postcss.plugin('', (_options = {}) => {
|
||||||
return (root) => {
|
return (root: Root) => {
|
||||||
root.walkRules((rule) => {
|
root.walkRules((rule) => {
|
||||||
if (change) return false
|
if (changes.length) return false
|
||||||
|
|
||||||
rule.walkAtRules('apply', (atRule) => {
|
rule.walkAtRules('apply', (atRule) => {
|
||||||
let { start, end } = atRule.source
|
let atRuleRange = postcssSourceToRange(atRule.source)
|
||||||
let atRuleRange: Range = {
|
|
||||||
start: {
|
|
||||||
line: start.line - 1,
|
|
||||||
character: start.column - 1,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
line: end.line - 1,
|
|
||||||
character: end.column - 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (cssRange) {
|
if (cssRange) {
|
||||||
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWithinRange(diagnostic.range.start, atRuleRange)) {
|
if (!isWithinRange(diagnostic.range.start, atRuleRange))
|
||||||
// keep looking
|
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
let ast = classNameToAst(
|
let ast = classNameToAst(
|
||||||
state,
|
state,
|
||||||
|
@ -270,35 +272,33 @@ async function provideInvalidApplyCodeActions(
|
||||||
diagnostic.className.classList.important
|
diagnostic.className.classList.important
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ast) {
|
if (!ast) return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.after(ast.nodes)
|
rule.after(ast.nodes)
|
||||||
let insertedRule = rule.next()
|
let insertedRule = rule.next()
|
||||||
|
if (!insertedRule) return false
|
||||||
|
|
||||||
if (totalClassNamesInClassList === 1) {
|
if (totalClassNamesInClassList === 1) {
|
||||||
atRule.remove()
|
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 outputIndent: string
|
||||||
let documentIndent = detectIndent(documentText)
|
let documentIndent = detectIndent(documentText)
|
||||||
|
|
||||||
let ruleRange: Range = {
|
changes.push({
|
||||||
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 = {
|
|
||||||
range: ruleRange,
|
range: ruleRange,
|
||||||
newText:
|
newText:
|
||||||
rule.toString() +
|
rule.toString() +
|
||||||
|
@ -315,7 +315,7 @@ async function provideInvalidApplyCodeActions(
|
||||||
documentIndent.indent
|
documentIndent.indent
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
}
|
})
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -327,7 +327,7 @@ async function provideInvalidApplyCodeActions(
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!change) {
|
if (!changes.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,20 +338,7 @@ async function provideInvalidApplyCodeActions(
|
||||||
diagnostics: [diagnostic],
|
diagnostics: [diagnostic],
|
||||||
edit: {
|
edit: {
|
||||||
changes: {
|
changes: {
|
||||||
[params.textDocument.uri]: [
|
[params.textDocument.uri]: changes,
|
||||||
...(totalClassNamesInClassList > 1
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
range: diagnostic.className.classList.range,
|
|
||||||
newText: removeRangesFromString(
|
|
||||||
diagnostic.className.classList.classList,
|
|
||||||
diagnostic.className.relativeRange
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
change,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
|
@ -1,516 +1,13 @@
|
||||||
import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
|
import { TextDocument } from 'vscode-languageserver'
|
||||||
import { State, Settings } from '../../util/state'
|
import { State } 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 { getDocumentSettings } from '../../util/getDocumentSettings'
|
import { getDocumentSettings } from '../../util/getDocumentSettings'
|
||||||
const dlv = require('dlv')
|
import { DiagnosticKind, AugmentedDiagnostic } from './types'
|
||||||
import semver from 'semver'
|
import { getUtilityConflictDiagnostics } from './getUtilityConflictDiagnostics'
|
||||||
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
|
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
|
||||||
import { absoluteRange } from '../../util/absoluteRange'
|
import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
|
||||||
import { isObject } from '../../../class-names/isObject'
|
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
|
||||||
import { stringToPath } from '../../util/stringToPath'
|
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
|
||||||
import { closest } from '../../util/closest'
|
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDiagnostics(
|
export async function getDiagnostics(
|
||||||
state: State,
|
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,
|
clearAllDiagnostics,
|
||||||
} from './providers/diagnostics/diagnosticsProvider'
|
} from './providers/diagnostics/diagnosticsProvider'
|
||||||
import { createEmitter } from '../lib/emitter'
|
import { createEmitter } from '../lib/emitter'
|
||||||
import { provideCodeActions } from './providers/codeActionProvider'
|
import { provideCodeActions } from './providers/codeActions/codeActionProvider'
|
||||||
|
|
||||||
let connection = createConnection(ProposedFeatures.all)
|
let connection = createConnection(ProposedFeatures.all)
|
||||||
let state: State = { enabled: false, emitter: createEmitter(connection) }
|
let state: State = { enabled: false, emitter: createEmitter(connection) }
|
||||||
|
|
Loading…
Reference in New Issue