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

View File

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