UNPKG

eslint-plugin-yaml

Version:
157 lines (138 loc) 4.48 kB
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 } }