taplo_cli/commands/
lint.rs

1use 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}