refactor diagnostics and add types

master
Brad Cornes 2020-06-18 10:54:01 +01:00
parent e79f72bda8
commit b79dbfc9f9
5 changed files with 253 additions and 111 deletions

View File

@ -4,10 +4,9 @@ import {
CodeActionKind, CodeActionKind,
Range, Range,
TextEdit, TextEdit,
Diagnostic,
} from 'vscode-languageserver' } from 'vscode-languageserver'
import { State } from '../../util/state' import { State } from '../../util/state'
import { findLast, findClassNamesInRange } from '../../util/find' import { findLast } from '../../util/find'
import { isWithinRange } from '../../util/isWithinRange' import { isWithinRange } from '../../util/isWithinRange'
import { getClassNameParts } from '../../util/getClassNameAtPosition' import { getClassNameParts } from '../../util/getClassNameAtPosition'
const dlv = require('dlv') const dlv = require('dlv')
@ -16,19 +15,50 @@ import { removeRangeFromString } from '../../util/removeRangeFromString'
import detectIndent from 'detect-indent' import detectIndent from 'detect-indent'
import { cssObjToAst } from '../../util/cssObjToAst' import { cssObjToAst } from '../../util/cssObjToAst'
import isObject from '../../../util/isObject' import isObject from '../../../util/isObject'
import { getDiagnostics } from '../diagnostics/diagnosticsProvider'
import { rangesEqual } from '../../util/rangesEqual'
import {
DiagnosticKind,
isInvalidApplyDiagnostic,
AugmentedDiagnostic,
InvalidApplyDiagnostic,
} from '../diagnostics/types'
export function provideCodeActions( async function getDiagnosticsFromCodeActionParams(
state: State,
params: CodeActionParams,
only?: DiagnosticKind[]
): Promise<AugmentedDiagnostic[]> {
let document = state.editor.documents.get(params.textDocument.uri)
let diagnostics = await getDiagnostics(state, document, only)
return params.context.diagnostics
.map((diagnostic) => {
return diagnostics.find((d) => {
return rangesEqual(d.range, diagnostic.range)
})
})
.filter(Boolean)
}
export async function provideCodeActions(
state: State, state: State,
params: CodeActionParams params: CodeActionParams
): Promise<CodeAction[]> { ): Promise<CodeAction[]> {
if (params.context.diagnostics.length === 0) { let codes = params.context.diagnostics
return null .map((diagnostic) => diagnostic.code)
} .filter(Boolean) as DiagnosticKind[]
let diagnostics = await getDiagnosticsFromCodeActionParams(
state,
params,
codes
)
return Promise.all( return Promise.all(
params.context.diagnostics diagnostics
.map((diagnostic) => { .map((diagnostic) => {
if (diagnostic.code === 'invalidApply') { if (isInvalidApplyDiagnostic(diagnostic)) {
return provideInvalidApplyCodeAction(state, params, diagnostic) return provideInvalidApplyCodeAction(state, params, diagnostic)
} }
@ -127,31 +157,14 @@ function classNameToAst(
async function provideInvalidApplyCodeAction( async function provideInvalidApplyCodeAction(
state: State, state: State,
params: CodeActionParams, params: CodeActionParams,
diagnostic: Diagnostic diagnostic: InvalidApplyDiagnostic
): Promise<CodeAction> { ): Promise<CodeAction> {
let document = state.editor.documents.get(params.textDocument.uri) let document = state.editor.documents.get(params.textDocument.uri)
let documentText = document.getText() let documentText = document.getText()
const { postcss } = state.modules const { postcss } = state.modules
let change: TextEdit let change: TextEdit
let documentClassNames = findClassNamesInRange( let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
document,
{
start: {
line: Math.max(0, diagnostic.range.start.line - 10),
character: 0,
},
end: { line: diagnostic.range.start.line + 10, character: 0 },
},
'css'
)
let documentClassName = documentClassNames.find((className) =>
isWithinRange(diagnostic.range.start, className.range)
)
if (!documentClassName) {
return null
}
let totalClassNamesInClassList = documentClassName.classList.classList.split(
/\s+/ /\s+/
).length ).length
@ -184,7 +197,7 @@ async function provideInvalidApplyCodeAction(
state, state,
className, className,
rule.selector, rule.selector,
documentClassName.classList.important diagnostic.className.classList.important
) )
if (!ast) { if (!ast) {
@ -250,10 +263,10 @@ async function provideInvalidApplyCodeAction(
...(totalClassNamesInClassList > 1 ...(totalClassNamesInClassList > 1
? [ ? [
{ {
range: documentClassName.classList.range, range: diagnostic.className.classList.range,
newText: removeRangeFromString( newText: removeRangeFromString(
documentClassName.classList.classList, diagnostic.className.classList.classList,
documentClassName.relativeRange diagnostic.className.relativeRange
), ),
}, },
] ]

View File

@ -1,95 +1,103 @@
import { import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
TextDocument, import { State, Settings } from '../../util/state'
Diagnostic, import { isCssDoc } from '../../util/css'
DiagnosticSeverity,
Range,
} from 'vscode-languageserver'
import { State, Settings } from '../util/state'
import { isCssDoc } from '../util/css'
import { import {
findClassNamesInRange, findClassNamesInRange,
findClassListsInDocument, findClassListsInDocument,
getClassNamesInClassList, getClassNamesInClassList,
findAll, findAll,
indexToPosition, indexToPosition,
} from '../util/find' } from '../../util/find'
import { getClassNameMeta } from '../util/getClassNameMeta' import { getClassNameMeta } from '../../util/getClassNameMeta'
import { getClassNameDecls } from '../util/getClassNameDecls' import { getClassNameDecls } from '../../util/getClassNameDecls'
import { equal } from '../../util/array' import { equal } 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 { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
import { absoluteRange } from '../util/absoluteRange' import { absoluteRange } from '../../util/absoluteRange'
import { isObject } from '../../class-names/isObject' import { isObject } from '../../../class-names/isObject'
import { stringToPath } from '../util/stringToPath' import { stringToPath } from '../../util/stringToPath'
import { closest } from '../util/closest' import { closest } from '../../util/closest'
import {
InvalidApplyDiagnostic,
DiagnosticKind,
UtilityConflictsDiagnostic,
InvalidScreenDiagnostic,
InvalidVariantDiagnostic,
InvalidConfigPathDiagnostic,
InvalidTailwindDirectiveDiagnostic,
AugmentedDiagnostic,
} from './types'
function getInvalidApplyDiagnostics( function getInvalidApplyDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): InvalidApplyDiagnostic[] {
let severity = settings.lint.invalidApply let severity = settings.lint.invalidApply
if (severity === 'ignore') return [] if (severity === 'ignore') return []
const classNames = findClassNamesInRange(document, undefined, 'css') const classNames = findClassNamesInRange(document, undefined, 'css')
let diagnostics: Diagnostic[] = classNames let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
.map(({ className, range }) => { const meta = getClassNameMeta(state, className.className)
const meta = getClassNameMeta(state, className) if (!meta) return null
if (!meta) return null
let message: string let message: string
if (Array.isArray(meta)) { if (Array.isArray(meta)) {
message = `'@apply' cannot be used with '${className}' because it is included in multiple rulesets.` message = `'@apply' cannot be used with '${className.className}' because it is included in multiple rulesets.`
} else if (meta.source !== 'utilities') { } else if (meta.source !== 'utilities') {
message = `'@apply' cannot be used with '${className}' because it is not a utility.` message = `'@apply' cannot be used with '${className.className}' because it is not a utility.`
} else if (meta.context && meta.context.length > 0) { } else if (meta.context && meta.context.length > 0) {
if (meta.context.length === 1) { if (meta.context.length === 1) {
message = `'@apply' cannot be used with '${className}' because it is nested inside of an at-rule ('${meta.context[0]}').` message = `'@apply' cannot be used with '${className.className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
} else { } else {
message = `'@apply' cannot be used with '${className}' because it is nested inside of at-rules (${meta.context message = `'@apply' cannot be used with '${
.map((c) => `'${c}'`) className.className
.join(', ')}).` }' because it is nested inside of at-rules (${meta.context
} .map((c) => `'${c}'`)
} else if (meta.pseudo && meta.pseudo.length > 0) { .join(', ')}).`
if (meta.pseudo.length === 1) {
message = `'@apply' cannot be used with '${className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
} else {
message = `'@apply' cannot be used with '${className}' because its definition includes pseudo-selectors (${meta.pseudo
.map((p) => `'${p}'`)
.join(', ')}).`
}
} }
} else if (meta.pseudo && meta.pseudo.length > 0) {
if (!message) return null if (meta.pseudo.length === 1) {
message = `'@apply' cannot be used with '${className.className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
return { } else {
severity: message = `'@apply' cannot be used with '${
severity === 'error' className.className
? DiagnosticSeverity.Error }' because its definition includes pseudo-selectors (${meta.pseudo
: DiagnosticSeverity.Warning, .map((p) => `'${p}'`)
range, .join(', ')}).`
message,
code: 'invalidApply',
} }
}) }
.filter(Boolean)
return diagnostics 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( function getUtilityConflictDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): UtilityConflictsDiagnostic[] {
let severity = settings.lint.utilityConflicts let severity = settings.lint.utilityConflicts
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = [] let diagnostics: UtilityConflictsDiagnostic[] = []
const classLists = findClassListsInDocument(state, document) const classLists = findClassListsInDocument(state, document)
classLists.forEach((classList) => { classLists.forEach((classList) => {
@ -115,6 +123,9 @@ function getUtilityConflictDiagnostics(
equal(meta.pseudo, otherMeta.pseudo) equal(meta.pseudo, otherMeta.pseudo)
) { ) {
diagnostics.push({ diagnostics.push({
code: DiagnosticKind.UtilityConflicts,
className,
otherClassName,
range: className.range, range: className.range,
severity: severity:
severity === 'error' severity === 'error'
@ -143,11 +154,11 @@ function getInvalidScreenDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): InvalidScreenDiagnostic[] {
let severity = settings.lint.invalidScreen let severity = settings.lint.invalidScreen
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = [] let diagnostics: InvalidScreenDiagnostic[] = []
let ranges: Range[] = [] let ranges: Range[] = []
if (isCssDoc(state, document)) { if (isCssDoc(state, document)) {
@ -178,6 +189,7 @@ function getInvalidScreenDiagnostics(
} }
diagnostics.push({ diagnostics.push({
code: DiagnosticKind.InvalidScreen,
range: absoluteRange( range: absoluteRange(
{ {
start: indexToPosition( start: indexToPosition(
@ -204,11 +216,11 @@ function getInvalidVariantDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): InvalidVariantDiagnostic[] {
let severity = settings.lint.invalidVariant let severity = settings.lint.invalidVariant
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = [] let diagnostics: InvalidVariantDiagnostic[] = []
let ranges: Range[] = [] let ranges: Range[] = []
if (isCssDoc(state, document)) { if (isCssDoc(state, document)) {
@ -244,6 +256,7 @@ function getInvalidVariantDiagnostics(
listStartIndex + variants.slice(0, i).join('').length listStartIndex + variants.slice(0, i).join('').length
diagnostics.push({ diagnostics.push({
code: DiagnosticKind.InvalidVariant,
range: absoluteRange( range: absoluteRange(
{ {
start: indexToPosition(text, variantStartIndex), start: indexToPosition(text, variantStartIndex),
@ -268,11 +281,11 @@ function getInvalidConfigPathDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): InvalidConfigPathDiagnostic[] {
let severity = settings.lint.invalidConfigPath let severity = settings.lint.invalidConfigPath
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = [] let diagnostics: InvalidConfigPathDiagnostic[] = []
let ranges: Range[] = [] let ranges: Range[] = []
if (isCssDoc(state, document)) { if (isCssDoc(state, document)) {
@ -381,6 +394,7 @@ function getInvalidConfigPathDiagnostics(
match.groups.quote.length match.groups.quote.length
diagnostics.push({ diagnostics.push({
code: DiagnosticKind.InvalidConfigPath,
range: absoluteRange( range: absoluteRange(
{ {
start: indexToPosition(text, startIndex), start: indexToPosition(text, startIndex),
@ -404,11 +418,11 @@ function getInvalidTailwindDirectiveDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): Diagnostic[] { ): InvalidTailwindDirectiveDiagnostic[] {
let severity = settings.lint.invalidTailwindDirective let severity = settings.lint.invalidTailwindDirective
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: Diagnostic[] = [] let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
let ranges: Range[] = [] let ranges: Range[] = []
if (isCssDoc(state, document)) { if (isCssDoc(state, document)) {
@ -446,6 +460,7 @@ function getInvalidTailwindDirectiveDiagnostics(
} }
diagnostics.push({ diagnostics.push({
code: DiagnosticKind.InvalidTailwindDirective,
range: absoluteRange( range: absoluteRange(
{ {
start: indexToPosition( start: indexToPosition(
@ -468,26 +483,48 @@ function getInvalidTailwindDirectiveDiagnostics(
return diagnostics return diagnostics
} }
export async function provideDiagnostics( export async function getDiagnostics(
state: State, state: State,
document: TextDocument document: TextDocument,
): Promise<void> { only: DiagnosticKind[] = [
DiagnosticKind.UtilityConflicts,
DiagnosticKind.InvalidApply,
DiagnosticKind.InvalidScreen,
DiagnosticKind.InvalidVariant,
DiagnosticKind.InvalidConfigPath,
DiagnosticKind.InvalidTailwindDirective,
]
): Promise<AugmentedDiagnostic[]> {
const settings = await getDocumentSettings(state, document) const settings = await getDocumentSettings(state, document)
const diagnostics: Diagnostic[] = settings.validate return settings.validate
? [ ? [
...getUtilityConflictDiagnostics(state, document, settings), ...(only.includes(DiagnosticKind.UtilityConflicts)
...getInvalidApplyDiagnostics(state, document, settings), ? getUtilityConflictDiagnostics(state, document, settings)
...getInvalidScreenDiagnostics(state, document, settings), : []),
...getInvalidVariantDiagnostics(state, document, settings), ...(only.includes(DiagnosticKind.InvalidApply)
...getInvalidConfigPathDiagnostics(state, document, settings), ? getInvalidApplyDiagnostics(state, document, settings)
...getInvalidTailwindDirectiveDiagnostics(state, document, settings), : []),
...(only.includes(DiagnosticKind.InvalidScreen)
? getInvalidScreenDiagnostics(state, document, settings)
: []),
...(only.includes(DiagnosticKind.InvalidVariant)
? getInvalidVariantDiagnostics(state, document, settings)
: []),
...(only.includes(DiagnosticKind.InvalidConfigPath)
? getInvalidConfigPathDiagnostics(state, document, settings)
: []),
...(only.includes(DiagnosticKind.InvalidTailwindDirective)
? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
: []),
] ]
: [] : []
}
export async function provideDiagnostics(state: State, document: TextDocument) {
state.editor.connection.sendDiagnostics({ state.editor.connection.sendDiagnostics({
uri: document.uri, uri: document.uri,
diagnostics, diagnostics: await getDiagnostics(state, document),
}) })
} }

View File

@ -0,0 +1,82 @@
import { Diagnostic } from 'vscode-languageserver'
import { DocumentClassName, DocumentClassList } from '../../util/state'
export enum DiagnosticKind {
UtilityConflicts = 'utilityConflicts',
InvalidApply = 'invalidApply',
InvalidScreen = 'invalidScreen',
InvalidVariant = 'invalidVariant',
InvalidConfigPath = 'invalidConfigPath',
InvalidTailwindDirective = 'invalidTailwindDirective',
}
export type UtilityConflictsDiagnostic = Diagnostic & {
code: DiagnosticKind.UtilityConflicts
className: DocumentClassName
otherClassName: DocumentClassName
}
export function isUtilityConflictsDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is UtilityConflictsDiagnostic {
return diagnostic.code === DiagnosticKind.UtilityConflicts
}
export type InvalidApplyDiagnostic = Diagnostic & {
code: DiagnosticKind.InvalidApply
className: DocumentClassName
}
export function isInvalidApplyDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is InvalidApplyDiagnostic {
return diagnostic.code === DiagnosticKind.InvalidApply
}
export type InvalidScreenDiagnostic = Diagnostic & {
code: DiagnosticKind.InvalidScreen
}
export function isInvalidScreenDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is InvalidScreenDiagnostic {
return diagnostic.code === DiagnosticKind.InvalidScreen
}
export type InvalidVariantDiagnostic = Diagnostic & {
code: DiagnosticKind.InvalidVariant
}
export function isInvalidVariantDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is InvalidVariantDiagnostic {
return diagnostic.code === DiagnosticKind.InvalidVariant
}
export type InvalidConfigPathDiagnostic = Diagnostic & {
code: DiagnosticKind.InvalidConfigPath
}
export function isInvalidConfigPathDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is InvalidConfigPathDiagnostic {
return diagnostic.code === DiagnosticKind.InvalidConfigPath
}
export type InvalidTailwindDirectiveDiagnostic = Diagnostic & {
code: DiagnosticKind.InvalidTailwindDirective
}
export function isInvalidTailwindDirectiveDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is InvalidTailwindDirectiveDiagnostic {
return diagnostic.code === DiagnosticKind.InvalidTailwindDirective
}
export type AugmentedDiagnostic =
| UtilityConflictsDiagnostic
| InvalidApplyDiagnostic
| InvalidScreenDiagnostic
| InvalidVariantDiagnostic
| InvalidConfigPathDiagnostic
| InvalidTailwindDirectiveDiagnostic

View File

@ -32,7 +32,7 @@ import {
provideDiagnostics, provideDiagnostics,
updateAllDiagnostics, updateAllDiagnostics,
clearAllDiagnostics, clearAllDiagnostics,
} from './providers/diagnosticsProvider' } from './providers/diagnostics/diagnosticsProvider'
import { createEmitter } from '../lib/emitter' import { createEmitter } from '../lib/emitter'
import { provideCodeActions } from './providers/codeActionProvider' import { provideCodeActions } from './providers/codeActionProvider'

View File

@ -0,0 +1,10 @@
import { Range } from 'vscode-languageserver'
export function rangesEqual(a: Range, b: Range): boolean {
return (
a.start.line === b.start.line &&
a.start.character === b.start.character &&
a.end.line === b.end.line &&
a.end.character === b.end.character
)
}