taplo_cli/commands/
lint.rs1use std::path::Path;
2
3use crate::{args::LintCommand, Taplo};
4use anyhow::{anyhow, Context};
5use codespan_reporting::files::SimpleFile;
6use serde_json::json;
7use taplo::parser;
8use taplo_common::{
9 environment::Environment,
10 schema::associations::{AssociationRule, SchemaAssociation, DEFAULT_CATALOGS},
11};
12use tokio::io::AsyncReadExt;
13use url::Url;
14
15impl<E: Environment> Taplo<E> {
16 pub async fn execute_lint(&mut self, cmd: LintCommand) -> Result<(), anyhow::Error> {
17 self.schemas
18 .cache()
19 .set_cache_path(cmd.general.cache_path.clone());
20
21 let config = self.load_config(&cmd.general).await?;
22
23 if !cmd.no_schema {
24 if let Some(schema_url) = cmd.schema.clone() {
25 self.schemas.associations().add(
26 AssociationRule::regex(".*")?,
27 SchemaAssociation {
28 meta: json!({"source": "command-line"}),
29 url: schema_url,
30 priority: 999,
31 },
32 );
33 } else {
34 self.schemas.associations().add_from_config(&config);
35
36 for catalog in &cmd.schema_catalog {
37 self.schemas
38 .associations()
39 .add_from_catalog(catalog)
40 .await
41 .with_context(|| "failed to load schema catalog")?;
42 }
43
44 if cmd.default_schema_catalogs {
45 for catalog in DEFAULT_CATALOGS {
46 self.schemas
47 .associations()
48 .add_from_catalog(&Url::parse(catalog).unwrap())
49 .await
50 .with_context(|| "failed to load schema catalog")?;
51 }
52 }
53 }
54 }
55
56 if matches!(cmd.files.get(0).map(|it| it.as_str()), Some("-")) {
57 self.lint_stdin(cmd).await
58 } else {
59 self.lint_files(cmd).await
60 }
61 }
62
63 #[tracing::instrument(skip_all)]
64 async fn lint_stdin(&self, _cmd: LintCommand) -> Result<(), anyhow::Error> {
65 let mut source = String::new();
66 self.env.stdin().read_to_string(&mut source).await?;
67 self.lint_source("-", &source).await
68 }
69
70 #[tracing::instrument(skip_all)]
71 async fn lint_files(&mut self, cmd: LintCommand) -> Result<(), anyhow::Error> {
72 let config = self.load_config(&cmd.general).await?;
73
74 let cwd = self
75 .env
76 .cwd_normalized()
77 .ok_or_else(|| anyhow!("could not figure the current working directory"))?;
78
79 let files = self
80 .collect_files(&cwd, &config, cmd.files.into_iter())
81 .await?;
82
83 let mut result = Ok(());
84
85 for file in files {
86 if let Err(error) = self.lint_file(&file).await {
87 tracing::error!(%error, path = ?file, "invalid file");
88 result = Err(anyhow!("some files were not valid"));
89 }
90 }
91
92 result
93 }
94
95 async fn lint_file(&self, file: &Path) -> Result<(), anyhow::Error> {
96 let source = self.env.read_file(file).await?;
97 let source = String::from_utf8(source)?;
98 self.lint_source(&file.to_string_lossy(), &source).await
99 }
100
101 async fn lint_source(&self, file_path: &str, source: &str) -> Result<(), anyhow::Error> {
102 let parse = parser::parse(source);
103
104 self.print_parse_errors(&SimpleFile::new(file_path, source), &parse.errors)
105 .await?;
106
107 if !parse.errors.is_empty() {
108 return Err(anyhow!("syntax errors found"));
109 }
110
111 let dom = parse.into_dom();
112
113 if let Err(errors) = dom.validate() {
114 self.print_semantic_errors(&SimpleFile::new(file_path, source), errors)
115 .await?;
116
117 return Err(anyhow!("semantic errors found"));
118 }
119
120 let config = self.config.as_ref().unwrap();
121
122 if !config.is_schema_enabled(Path::new(file_path)) {
123 tracing::debug!("schema validation disabled for config file");
124 return Ok(());
125 }
126
127 let file_uri: Url = format!("file://{file_path}").parse().unwrap();
128
129 self.schemas
130 .associations()
131 .add_from_document(&file_uri, &dom);
132
133 if let Some(schema_association) = self.schemas.associations().association_for(&file_uri) {
134 tracing::debug!(
135 schema.url = %schema_association.url,
136 schema.name = schema_association.meta["name"].as_str().unwrap_or(""),
137 schema.source = schema_association.meta["source"].as_str().unwrap_or(""),
138 "using schema"
139 );
140
141 let errors = self
142 .schemas
143 .validate_root(&schema_association.url, &dom)
144 .await?;
145
146 if !errors.is_empty() {
147 self.print_schema_errors(&SimpleFile::new(file_path, source), &errors)
148 .await?;
149
150 return Err(anyhow!("schema validation failed"));
151 }
152 }
153
154 Ok(())
155 }
156}