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, TextDocument,
Diagnostic, Diagnostic,
DiagnosticSeverity, DiagnosticSeverity,
Range,
} from 'vscode-languageserver' } from 'vscode-languageserver'
import { State, Settings } from '../util/state' import { State, Settings } from '../util/state'
import { isCssDoc } from '../util/css' import { isCssDoc } from '../util/css'
@ -18,6 +19,8 @@ import { equal, flatten } from '../../util/array'
import { getDocumentSettings } from '../util/getDocumentSettings' import { getDocumentSettings } from '../util/getDocumentSettings'
const dlv = require('dlv') const dlv = require('dlv')
import semver from 'semver' import semver from 'semver'
import { getLanguageBoundaries } from '../util/getLanguageBoundaries'
import { absoluteRange } from '../util/absoluteRange'
function getUnsupportedApplyDiagnostics( function getUnsupportedApplyDiagnostics(
state: State, state: State,
@ -140,35 +143,51 @@ function getUnknownScreenDiagnostics(
let severity = settings.lint.unknownScreen let severity = settings.lint.unknownScreen
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let text = document.getText() let diagnostics: Diagnostic[] = []
let matches = findAll(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text) let ranges: Range[] = []
let screens = Object.keys( if (isCssDoc(state, document)) {
dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {})) ranges.push(undefined)
) } else {
let boundaries = getLanguageBoundaries(state, document)
if (!boundaries) return []
ranges.push(...boundaries.css)
}
return matches ranges.forEach((range) => {
.map((match) => { 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)) { if (screens.includes(match.groups.screen)) {
return null return null
} }
return { diagnostics.push({
range: { range: absoluteRange(
start: indexToPosition( {
text, start: indexToPosition(
match.index + match[0].length - match.groups.screen.length text,
), match.index + match[0].length - match.groups.screen.length
end: indexToPosition(text, match.index + match[0].length), ),
}, end: indexToPosition(text, match.index + match[0].length),
},
range
),
severity: severity:
severity === 'error' severity === 'error'
? DiagnosticSeverity.Error ? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning, : DiagnosticSeverity.Warning,
message: 'Unknown screen', message: 'Unknown screen',
} })
}) })
.filter(Boolean) })
return diagnostics
} }
function getUnknownVariantDiagnostics( function getUnknownVariantDiagnostics(
@ -179,43 +198,54 @@ function getUnknownVariantDiagnostics(
let severity = settings.lint.unknownVariant let severity = settings.lint.unknownVariant
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let text = document.getText() let diagnostics: Diagnostic[] = []
let matches = findAll(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text) let ranges: Range[] = []
return flatten( if (isCssDoc(state, document)) {
matches ranges.push(undefined)
.map((match) => { } else {
let diagnostics: Diagnostic[] = [] let boundaries = getLanguageBoundaries(state, document)
let variants = match.groups.variants.split(/(\s*,\s*)/) if (!boundaries) return []
let listStartIndex = ranges.push(...boundaries.css)
match.index + match[0].length - match.groups.variants.length }
for (let i = 0; i < variants.length; i += 2) { ranges.forEach((range) => {
let variant = variants[i].trim() let text = document.getText(range)
if (state.variants.includes(variant)) { let matches = findAll(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
continue
}
let variantStartIndex = matches.forEach((match) => {
listStartIndex + variants.slice(0, i).join('').length let variants = match.groups.variants.split(/(\s*,\s*)/)
let listStartIndex =
match.index + match[0].length - match.groups.variants.length
diagnostics.push({ for (let i = 0; i < variants.length; i += 2) {
range: { 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), start: indexToPosition(text, variantStartIndex),
end: indexToPosition(text, variantStartIndex + variant.length), end: indexToPosition(text, variantStartIndex + variant.length),
}, },
severity: range
severity === 'error' ),
? DiagnosticSeverity.Error severity:
: DiagnosticSeverity.Warning, severity === 'error'
message: `Unknown variant: ${variant}`, ? DiagnosticSeverity.Error
}) : DiagnosticSeverity.Warning,
} message: `Unknown variant: ${variant}`,
})
}
})
})
return diagnostics return diagnostics
})
.filter(Boolean)
)
} }
function getUnknownConfigKeyDiagnostics( function getUnknownConfigKeyDiagnostics(
@ -226,14 +256,25 @@ function getUnknownConfigKeyDiagnostics(
let severity = settings.lint.unknownConfigKey let severity = settings.lint.unknownConfigKey
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let text = document.getText() let diagnostics: Diagnostic[] = []
let matches = findAll( let ranges: Range[] = []
/(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/g,
text
)
return matches if (isCssDoc(state, document)) {
.map((match) => { 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 base = match.groups.helper === 'theme' ? ['theme'] : []
let keys = match.groups.key.split(/[.\[\]]/).filter(Boolean) let keys = match.groups.key.split(/[.\[\]]/).filter(Boolean)
let value = dlv(state.config, [...base, ...keys]) let value = dlv(state.config, [...base, ...keys])
@ -251,19 +292,24 @@ function getUnknownConfigKeyDiagnostics(
1 + // open paren 1 + // open paren
match.groups.quote.length match.groups.quote.length
return { diagnostics.push({
range: { range: absoluteRange(
start: indexToPosition(text, startIndex), {
end: indexToPosition(text, startIndex + match.groups.key.length), start: indexToPosition(text, startIndex),
}, end: indexToPosition(text, startIndex + match.groups.key.length),
},
range
),
severity: severity:
severity === 'error' severity === 'error'
? DiagnosticSeverity.Error ? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning, : DiagnosticSeverity.Warning,
message: `Unknown ${match.groups.helper} key: ${match.groups.key}`, message: `Unknown ${match.groups.helper} key: ${match.groups.key}`,
} })
}) })
.filter(Boolean) })
return diagnostics
} }
function getUnsupportedTailwindDirectiveDiagnostics( function getUnsupportedTailwindDirectiveDiagnostics(
@ -274,30 +320,44 @@ function getUnsupportedTailwindDirectiveDiagnostics(
let severity = settings.lint.unsupportedTailwindDirective let severity = settings.lint.unsupportedTailwindDirective
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let text = document.getText() let diagnostics: Diagnostic[] = []
let matches = findAll(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text) let ranges: Range[] = []
let allowed = [ if (isCssDoc(state, document)) {
'utilities', ranges.push(undefined)
'components', } else {
'screens', let boundaries = getLanguageBoundaries(state, document)
semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight', if (!boundaries) return []
] ranges.push(...boundaries.css)
}
return matches ranges.forEach((range) => {
.map((match) => { let text = document.getText(range)
if (allowed.includes(match.groups.value)) { 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 return null
} }
return { diagnostics.push({
range: { range: absoluteRange(
start: indexToPosition( {
text, start: indexToPosition(
match.index + match[0].length - match.groups.value.length text,
), match.index + match[0].length - match.groups.value.length
end: indexToPosition(text, match.index + match[0].length), ),
}, end: indexToPosition(text, match.index + match[0].length),
},
range
),
severity: severity:
severity === 'error' severity === 'error'
? DiagnosticSeverity.Error ? DiagnosticSeverity.Error
@ -305,9 +365,11 @@ function getUnsupportedTailwindDirectiveDiagnostics(
message: `Unsupported value: ${match.groups.value}${ message: `Unsupported value: ${match.groups.value}${
match.groups.value === 'preflight' ? '. Use base instead.' : '' match.groups.value === 'preflight' ? '. Use base instead.' : ''
}`, }`,
} })
}) })
.filter(Boolean) })
return diagnostics
} }
export async function provideDiagnostics( export async function provideDiagnostics(
@ -319,19 +381,15 @@ export async function provideDiagnostics(
const diagnostics: Diagnostic[] = settings.validate const diagnostics: Diagnostic[] = settings.validate
? [ ? [
...getUtilityConflictDiagnostics(state, document, settings), ...getUtilityConflictDiagnostics(state, document, settings),
...(isCssDoc(state, document) ...getUnsupportedApplyDiagnostics(state, document, settings),
? [ ...getUnknownScreenDiagnostics(state, document, settings),
...getUnsupportedApplyDiagnostics(state, document, settings), ...getUnknownVariantDiagnostics(state, document, settings),
...getUnknownScreenDiagnostics(state, document, settings), ...getUnknownConfigKeyDiagnostics(state, document, settings),
...getUnknownVariantDiagnostics(state, document, settings), ...getUnsupportedTailwindDirectiveDiagnostics(
...getUnknownConfigKeyDiagnostics(state, document, settings), state,
...getUnsupportedTailwindDirectiveDiagnostics( document,
state, settings
document, ),
settings
),
]
: []),
] ]
: [] : []

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, getClassAttributeLexer,
getComputedClassAttributeLexer, getComputedClassAttributeLexer,
} from './lexers' } from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] { export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray let match: RegExpMatchArray
@ -230,70 +231,13 @@ export function findClassListsInDocument(
return findClassListsInCssRange(doc) return findClassListsInCssRange(doc)
} }
if (isVueDoc(doc)) { let boundaries = getLanguageBoundaries(state, doc)
let text = doc.getText() if (!boundaries) return []
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)),
]
)
}
if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) { return flatten([
let text = doc.getText() ...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
let styleBlocks = findAll(/<style(?:\s[^>]*>|>).*?(<\/style>|$)/gis, text) ...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
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 []
} }
export function indexToPosition(str: string, index: number): Position { 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
}