eslint-plugin-yaml
Version:
157 lines (138 loc) • 4.48 kB
text/typescript
import type { Linter, AST } from "eslint"
import type * as estree from "estree"
import { valueToEstree } from "estree-util-value-to-estree"
import { loadAll, type YAMLException } from "js-yaml"
import { type LintError as JsHintLintErrors, JSHINT as jshint } from "jshint"
import path from "path"
// it seems mark can be undefined issue #67
type LoadYamlException = {
message: string
mark?: { buffer: string; line: number; column: number }
}
type LoadYamlValue = {
value: unknown[]
messages: Linter.LintMessage[]
}
type Path = string
export class YamlProcessor implements Linter.Processor<Linter.ProcessorFile> {
/** Map of file paths to their YAML value and warnings. */
private parsedFiles = new Map<Path, LoadYamlValue>()
// TODO: Implement autofix
public supportsAutofix = false
/** The preprocess method is called by ESLint before the parser is run. */
public preprocess(text: string, path: Path): Linter.ProcessorFile[] {
return [{ text, filename: path }]
}
/** Parser for YAML files. */
public parseForESLint(code: string, options?: { filePath?: Path }): Linter.ESLintParseResult {
// initialize AST
const ast: AST.Program = {
type: "Program",
body: [],
sourceType: "script",
tokens: [],
comments: [],
range: [0, code.length],
loc: {
start: { line: 1, column: 0 },
end: {
line: code.split("\n")?.length ?? 1,
column: code.split("\n")?.slice(-1)?.[0]?.length ?? 0,
},
},
}
try {
// load YAML
const { yamlDocs, warnings } = this.loadYaml(code, options?.filePath)
// build AST
const statement: estree.ExpressionStatement = {
type: "ExpressionStatement",
expression: valueToEstree(yamlDocs),
}
ast.body.push(statement)
// save warnings for postprocess
this.parsedFiles.set(code, {
value: yamlDocs,
messages: warnings.map((warning) => ({
ruleId: "yaml-warning",
severity: 2,
message: warning.message,
source: warning.mark?.buffer,
line: warning.mark?.line ?? 0,
column: warning.mark?.column ?? 0,
})),
})
} catch (err) {
const { message, mark } = err as LoadYamlException
const key = options?.filePath ?? code
this.parsedFiles.set(key, {
value: [],
messages: [
{
ruleId: "invalid-yaml",
severity: 2,
message,
line: mark?.line ?? 0,
column: mark?.column ?? 0,
},
],
})
}
return { ast }
}
/** The postprocess method is called by ESLint after the parser is run. */
public postprocess(_messages: Linter.LintMessage[][], filePath: Path): Linter.LintMessage[] {
if (!this.isYaml(filePath)) {
return []
}
let value = this.parsedFiles.get(filePath)
if (value === undefined) {
// search all the keys that end with filePath
const key = Array.from(this.parsedFiles.keys()).find((key) => key.endsWith(filePath))
if (key === undefined) {
return [] // no value found
}
value = this.parsedFiles.get(key)!
}
if (value.messages.length > 0) {
this.parsedFiles.delete(filePath)
return value.messages
}
const errors = value.value.flatMap((yamlDoc) => this.lintJSON(yamlDoc))
const linter_messages: Linter.LintMessage[] = errors.map((error) => {
const { reason, evidence, line, character } = error
return {
ruleId: "bad-yaml",
severity: 2,
message: reason,
source: evidence,
line,
column: character,
}
})
this.parsedFiles.delete(filePath)
return linter_messages
}
private isYaml(filePath: Path) {
const fileExtension = path.extname(filePath)
return [".yaml", ".yml"].includes(fileExtension)
}
private loadYaml(fileContent: string, filePath?: Path) {
const warnings: YAMLException[] = []
const yamlDocs = loadAll(fileContent, undefined, {
filename: filePath,
json: false,
onWarning: (exception) => {
warnings.push(exception)
},
})
return { yamlDocs, warnings }
}
private lintJSON(yamlDoc: LoadYamlValue["value"][0]): JsHintLintErrors[] {
const yaml_json = JSON.stringify(yamlDoc, null, 2)
jshint(yaml_json)
const data = jshint.data()
const errors = data?.errors ?? []
return errors
}
}