enable diagnostics in multi-language documents

master
Brad Cornes 2020-06-15 11:24:05 +01:00
parent 81aad28bc8
commit 4f17e2358d
4 changed files with 252 additions and 157 deletions

View File

@ -2,6 +2,7 @@ import {
TextDocument,
Diagnostic,
DiagnosticSeverity,
Range,
} from 'vscode-languageserver'
import { State, Settings } from '../util/state'
import { isCssDoc } from '../util/css'
@ -18,6 +19,8 @@ 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,
@ -140,35 +143,51 @@ function getUnknownScreenDiagnostics(
let severity = settings.lint.unknownScreen
if (severity === 'ignore') return []
let text = document.getText()
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', {}))
)
return matches
.map((match) => {
matches.forEach((match) => {
if (screens.includes(match.groups.screen)) {
return null
}
return {
range: {
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',
}
})
.filter(Boolean)
})
})
return diagnostics
}
function getUnknownVariantDiagnostics(
@ -179,13 +198,22 @@ function getUnknownVariantDiagnostics(
let severity = settings.lint.unknownVariant
if (severity === 'ignore') return []
let text = document.getText()
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)
return flatten(
matches
.map((match) => {
let diagnostics: Diagnostic[] = []
matches.forEach((match) => {
let variants = match.groups.variants.split(/(\s*,\s*)/)
let listStartIndex =
match.index + match[0].length - match.groups.variants.length
@ -200,10 +228,13 @@ function getUnknownVariantDiagnostics(
listStartIndex + variants.slice(0, i).join('').length
diagnostics.push({
range: {
range: absoluteRange(
{
start: indexToPosition(text, variantStartIndex),
end: indexToPosition(text, variantStartIndex + variant.length),
},
range
),
severity:
severity === 'error'
? DiagnosticSeverity.Error
@ -211,11 +242,10 @@ function getUnknownVariantDiagnostics(
message: `Unknown variant: ${variant}`,
})
}
})
})
return diagnostics
})
.filter(Boolean)
)
}
function getUnknownConfigKeyDiagnostics(
@ -226,14 +256,25 @@ function getUnknownConfigKeyDiagnostics(
let severity = settings.lint.unknownConfigKey
if (severity === 'ignore') return []
let text = document.getText()
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
)
return matches
.map((match) => {
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])
@ -251,19 +292,24 @@ function getUnknownConfigKeyDiagnostics(
1 + // open paren
match.groups.quote.length
return {
range: {
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: `Unknown ${match.groups.helper} key: ${match.groups.key}`,
}
})
.filter(Boolean)
})
})
return diagnostics
}
function getUnsupportedTailwindDirectiveDiagnostics(
@ -274,30 +320,44 @@ function getUnsupportedTailwindDirectiveDiagnostics(
let severity = settings.lint.unsupportedTailwindDirective
if (severity === 'ignore') return []
let text = document.getText()
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 allowed = [
let valid = [
'utilities',
'components',
'screens',
semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
]
return matches
.map((match) => {
if (allowed.includes(match.groups.value)) {
matches.forEach((match) => {
if (valid.includes(match.groups.value)) {
return null
}
return {
range: {
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
@ -305,9 +365,11 @@ function getUnsupportedTailwindDirectiveDiagnostics(
message: `Unsupported value: ${match.groups.value}${
match.groups.value === 'preflight' ? '. Use base instead.' : ''
}`,
}
})
.filter(Boolean)
})
})
return diagnostics
}
export async function provideDiagnostics(
@ -319,8 +381,6 @@ export async function provideDiagnostics(
const diagnostics: Diagnostic[] = settings.validate
? [
...getUtilityConflictDiagnostics(state, document, settings),
...(isCssDoc(state, document)
? [
...getUnsupportedApplyDiagnostics(state, document, settings),
...getUnknownScreenDiagnostics(state, document, settings),
...getUnknownVariantDiagnostics(state, document, settings),
@ -331,8 +391,6 @@ export async function provideDiagnostics(
settings
),
]
: []),
]
: []
state.editor.connection.sendDiagnostics({

View File

@ -0,0 +1,18 @@
import { Range } from 'vscode-languageserver'
export function absoluteRange(range: Range, reference?: Range) {
return {
start: {
line: (reference?.start.line || 0) + range.start.line,
character:
(range.end.line === 0 ? reference?.start.character || 0 : 0) +
range.start.character,
},
end: {
line: (reference?.start.line || 0) + range.end.line,
character:
(range.end.line === 0 ? reference?.start.character || 0 : 0) +
range.end.character,
},
}
}

View File

@ -10,6 +10,7 @@ import {
getClassAttributeLexer,
getComputedClassAttributeLexer,
} from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray
@ -230,70 +231,13 @@ export function findClassListsInDocument(
return findClassListsInCssRange(doc)
}
if (isVueDoc(doc)) {
let text = doc.getText()
let blocks = findAll(
/<(?<type>template|style|script)\b[^>]*>.*?(<\/\k<type>>|$)/gis,
text
)
let htmlRanges: Range[] = []
let cssRanges: Range[] = []
for (let i = 0; i < blocks.length; i++) {
let range = {
start: indexToPosition(text, blocks[i].index),
end: indexToPosition(text, blocks[i].index + blocks[i][0].length),
}
if (blocks[i].groups.type === 'style') {
cssRanges.push(range)
} else {
htmlRanges.push(range)
}
}
return [].concat.apply(
[],
[
...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
]
)
}
let boundaries = getLanguageBoundaries(state, doc)
if (!boundaries) return []
if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) {
let text = doc.getText()
let styleBlocks = findAll(/<style(?:\s[^>]*>|>).*?(<\/style>|$)/gis, text)
let htmlRanges: Range[] = []
let cssRanges: Range[] = []
let currentIndex = 0
for (let i = 0; i < styleBlocks.length; i++) {
htmlRanges.push({
start: indexToPosition(text, currentIndex),
end: indexToPosition(text, styleBlocks[i].index),
})
cssRanges.push({
start: indexToPosition(text, styleBlocks[i].index),
end: indexToPosition(
text,
styleBlocks[i].index + styleBlocks[i][0].length
),
})
currentIndex = styleBlocks[i].index + styleBlocks[i][0].length
}
htmlRanges.push({
start: indexToPosition(text, currentIndex),
end: indexToPosition(text, text.length),
})
return [].concat.apply(
[],
[
...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
]
)
}
return []
return flatten([
...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
])
}
export function indexToPosition(str: string, index: number): Position {

View File

@ -0,0 +1,75 @@
import { TextDocument, Range } from 'vscode-languageserver'
import { isVueDoc, isHtmlDoc, isSvelteDoc } from './html'
import { State } from './state'
import { findAll, indexToPosition } from './find'
import { isJsDoc } from './js'
export interface LanguageBoundaries {
html: Range[]
css: Range[]
}
export function getLanguageBoundaries(
state: State,
doc: TextDocument
): LanguageBoundaries | null {
if (isVueDoc(doc)) {
let text = doc.getText()
let blocks = findAll(
/<(?<type>template|style|script)\b[^>]*>.*?(<\/\k<type>>|$)/gis,
text
)
let htmlRanges: Range[] = []
let cssRanges: Range[] = []
for (let i = 0; i < blocks.length; i++) {
let range = {
start: indexToPosition(text, blocks[i].index),
end: indexToPosition(text, blocks[i].index + blocks[i][0].length),
}
if (blocks[i].groups.type === 'style') {
cssRanges.push(range)
} else {
htmlRanges.push(range)
}
}
return {
html: htmlRanges,
css: cssRanges,
}
}
if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) {
let text = doc.getText()
let styleBlocks = findAll(/<style(?:\s[^>]*>|>).*?(<\/style>|$)/gis, text)
let htmlRanges: Range[] = []
let cssRanges: Range[] = []
let currentIndex = 0
for (let i = 0; i < styleBlocks.length; i++) {
htmlRanges.push({
start: indexToPosition(text, currentIndex),
end: indexToPosition(text, styleBlocks[i].index),
})
cssRanges.push({
start: indexToPosition(text, styleBlocks[i].index),
end: indexToPosition(
text,
styleBlocks[i].index + styleBlocks[i][0].length
),
})
currentIndex = styleBlocks[i].index + styleBlocks[i][0].length
}
htmlRanges.push({
start: indexToPosition(text, currentIndex),
end: indexToPosition(text, text.length),
})
return {
html: htmlRanges,
css: cssRanges,
}
}
return null
}