on master help sign in
feat: xtask themelint (#3234)

* feat: cargo xtask themelint <theme> * fix: add docs and print json error status * fix: refactor paths -> path * fix: remove unused function * fix: only report one err per scope (ui.statusline is reported if none of ui.statusline.* is recognized) * fix: save work for later * fix: finally decided on a design * fix: ready for discussion * fix: better rules * fix: lint precision * fix: String -> &'static str * fix: allowlist not denylist for file type * fix: add missing and indication of what's needed * fix: copy pasteable errors * fix: use Loader:read_names * Update xtask/src/helpers.rs Co-authored-by: Ivan Tham <pickfire@riseup.net> * fix: remove into and clone for str * Update book/src/themes.md Co-authored-by: Ivan Tham <pickfire@riseup.net> * fix: better lint output * fix: cleaner logic for lint reporting * style: use explicit imports * Pascal support (#3542) * fix: add difference check for statusline normal,insert,select * fix: fg for whitespace and early exit if and one is ok * chore: cleaning up, no idea how these got here or how this will look * chore: revert from older commit? * refactor: use static fn to equalize api between difference and existance * refactor: querycheck and clippy * refactor: clippy fixes * fix: query-check behaves as before * fix: error with x of y message, not total count * fix: consistent reporting and less mutable state * fix: selection difference ref #3942 ref #1833 Co-authored-by: Ivan Tham <pickfire@riseup.net> Co-authored-by: ath3 <45574139+ath3@users.noreply.github.com>

commit08b2ecc99a1ac1c461117bf6c79b77484eb433e4 authorAlexander Brevig <alexanderbrevig@gmail.com> dateMon 19 Sep 2022, 15:38 (4 years ago) parent5ea7855
9 files changed+451−238
Cargo.lock+1 book/src/themes.md+6 xtask/Cargo.toml+1 xtask/src/docgen.rs+117 xtask/src/helpers.rs+44 xtask/src/main.rs+20−238 xtask/src/path.rs+24 xtask/src/querycheck.rs+39 xtask/src/themelint.rs+199
Cargo.lock+1
@@ -1469,5 +1469,6 @@ dependencies = [
14691469 "helix-core",
14701470 "helix-loader",
14711471 "helix-term",
1472+ "helix-view",
14721473 "toml",
14731474]
book/src/themes.md+6
@@ -254,4 +254,10 @@ These scopes are used for theming the editor interface.
254254| `diagnostic.warning` | Diagnostics warning (editing area) |
255255| `diagnostic.error` | Diagnostics error (editing area) |
256256
257+You can check compliance to spec with
258+
259+```shell
260+cargo xtask themelint onedark # replace onedark with <name>
261+```
262+
257263[editor-section]: ./configuration.md#editor-section
xtask/Cargo.toml+1
@@ -8,5 +8,6 @@ edition = "2021"
88[dependencies]
99helix-term = { version = "0.6", path = "../helix-term" }
1010helix-core = { version = "0.6", path = "../helix-core" }
11+helix-view = { version = "0.6", path = "../helix-view" }
1112helix-loader = { version = "0.6", path = "../helix-loader" }
1213toml = "0.5"
xtask/src/docgen.rsnew file+117
@@ -0,0 +1,117 @@
1+use crate::helpers;
2+use crate::path;
3+use crate::DynError;
4+
5+use helix_term::commands::TYPABLE_COMMAND_LIST;
6+use helix_term::health::TsFeature;
7+use std::fs;
8+
9+pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
10+pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
11+
12+fn md_table_heading(cols: &[String]) -> String {
13+ let mut header = String::new();
14+ header += &md_table_row(cols);
15+ header += &md_table_row(&vec!["---".to_string(); cols.len()]);
16+ header
17+}
18+
19+fn md_table_row(cols: &[String]) -> String {
20+ format!("| {} |\n", cols.join(" | "))
21+}
22+
23+fn md_mono(s: &str) -> String {
24+ format!("`{}`", s)
25+}
26+
27+pub fn typable_commands() -> Result<String, DynError> {
28+ let mut md = String::new();
29+ md.push_str(&md_table_heading(&[
30+ "Name".to_owned(),
31+ "Description".to_owned(),
32+ ]));
33+
34+ let cmdify = |s: &str| format!("`:{}`", s);
35+
36+ for cmd in TYPABLE_COMMAND_LIST {
37+ let names = std::iter::once(&cmd.name)
38+ .chain(cmd.aliases.iter())
39+ .map(|a| cmdify(a))
40+ .collect::<Vec<_>>()
41+ .join(", ");
42+
43+ let doc = cmd.doc.replace('\n', "<br>");
44+
45+ md.push_str(&md_table_row(&[names.to_owned(), doc.to_owned()]));
46+ }
47+
48+ Ok(md)
49+}
50+
51+pub fn lang_features() -> Result<String, DynError> {
52+ let mut md = String::new();
53+ let ts_features = TsFeature::all();
54+
55+ let mut cols = vec!["Language".to_owned()];
56+ cols.append(
57+ &mut ts_features
58+ .iter()
59+ .map(|t| t.long_title().to_string())
60+ .collect::<Vec<_>>(),
61+ );
62+ cols.push("Default LSP".to_owned());
63+
64+ md.push_str(&md_table_heading(&cols));
65+ let config = helpers::lang_config();
66+
67+ let mut langs = config
68+ .language
69+ .iter()
70+ .map(|l| l.language_id.clone())
71+ .collect::<Vec<_>>();
72+ langs.sort_unstable();
73+
74+ let mut ts_features_to_langs = Vec::new();
75+ for &feat in ts_features {
76+ ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
77+ }
78+
79+ let mut row = Vec::new();
80+ for lang in langs {
81+ let lc = config
82+ .language
83+ .iter()
84+ .find(|l| l.language_id == lang)
85+ .unwrap(); // lang comes from config
86+ row.push(lc.language_id.clone());
87+
88+ for (_feat, support_list) in &ts_features_to_langs {
89+ row.push(
90+ if support_list.contains(&lang) {
91+ "✓"
92+ } else {
93+ ""
94+ }
95+ .to_owned(),
96+ );
97+ }
98+ row.push(
99+ lc.language_server
100+ .as_ref()
101+ .map(|s| s.command.clone())
102+ .map(|c| md_mono(&c))
103+ .unwrap_or_default(),
104+ );
105+
106+ md.push_str(&md_table_row(&row));
107+ row.clear();
108+ }
109+
110+ Ok(md)
111+}
112+
113+pub fn write(filename: &str, data: &str) {
114+ let error = format!("Could not write to {}", filename);
115+ let path = path::book_gen().join(filename);
116+ fs::write(path, data).expect(&error);
117+}
xtask/src/helpers.rsnew file+44
@@ -0,0 +1,44 @@
1+use std::path::{Path, PathBuf};
2+
3+use crate::path;
4+use helix_core::syntax::Configuration as LangConfig;
5+use helix_term::health::TsFeature;
6+
7+/// Get the list of languages that support a particular tree-sitter
8+/// based feature.
9+pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
10+ let queries_dir = path::ts_queries();
11+
12+ find_files(&queries_dir, feat.runtime_filename())
13+ .iter()
14+ .map(|f| {
15+ // .../helix/runtime/queries/python/highlights.scm
16+ let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
17+ let lang = tail.components().next().unwrap(); // python
18+ lang.as_os_str().to_string_lossy().to_string()
19+ })
20+ .collect()
21+}
22+
23+// naive implementation, but suffices for our needs
24+pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
25+ std::fs::read_dir(dir)
26+ .unwrap()
27+ .filter_map(|entry| {
28+ let path = entry.ok()?.path();
29+ if path.is_dir() {
30+ Some(find_files(&path, filename))
31+ } else if path.file_name()?.to_string_lossy() == filename {
32+ Some(vec![path])
33+ } else {
34+ None
35+ }
36+ })
37+ .flatten()
38+ .collect()
39+}
40+
41+pub fn lang_config() -> LangConfig {
42+ let bytes = std::fs::read(path::lang_config()).unwrap();
43+ toml::from_slice(&bytes).unwrap()
44+}
xtask/src/main.rs+20−238
@@ -1,255 +1,35 @@
1+mod docgen;
2+mod helpers;
3+mod path;
4+mod querycheck;
5+mod themelint;
6+
17use std::{env, error::Error};
28
39type DynError = Box<dyn Error>;
410
5−pub mod helpers {
6− use std::path::{Path, PathBuf};
7−
8− use crate::path;
9− use helix_core::syntax::Configuration as LangConfig;
10− use helix_term::health::TsFeature;
11−
12− /// Get the list of languages that support a particular tree-sitter
13− /// based feature.
14− pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
15− let queries_dir = path::ts_queries();
16−
17− find_files(&queries_dir, feat.runtime_filename())
18− .iter()
19− .map(|f| {
20− // .../helix/runtime/queries/python/highlights.scm
21− let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
22− let lang = tail.components().next().unwrap(); // python
23− lang.as_os_str().to_string_lossy().to_string()
24− })
25− .collect()
26− }
27−
28− /// Get the list of languages that have any form of tree-sitter
29− /// queries defined in the runtime directory.
30− pub fn langs_with_ts_queries() -> Vec<String> {
31− std::fs::read_dir(path::ts_queries())
32− .unwrap()
33− .filter_map(|entry| {
34− let entry = entry.ok()?;
35− entry
36− .file_type()
37− .ok()?
38− .is_dir()
39− .then(|| entry.file_name().to_string_lossy().to_string())
40− })
41− .collect()
42− }
43−
44− // naive implementation, but suffices for our needs
45− pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
46− std::fs::read_dir(dir)
47− .unwrap()
48− .filter_map(|entry| {
49− let path = entry.ok()?.path();
50− if path.is_dir() {
51− Some(find_files(&path, filename))
52− } else {
53− (path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
54− }
55− })
56− .flatten()
57− .collect()
58− }
59−
60− pub fn lang_config() -> LangConfig {
61− let bytes = std::fs::read(path::lang_config()).unwrap();
62− toml::from_slice(&bytes).unwrap()
63− }
64−}
65−
66−pub mod md_gen {
67− use crate::DynError;
68−
69− use crate::helpers;
70− use crate::path;
71− use helix_term::commands::TYPABLE_COMMAND_LIST;
72− use helix_term::health::TsFeature;
73− use std::fs;
74−
75− pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
76− pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
77−
78− fn md_table_heading(cols: &[String]) -> String {
79− let mut header = String::new();
80− header += &md_table_row(cols);
81− header += &md_table_row(&vec!["---".to_string(); cols.len()]);
82− header
83− }
84−
85− fn md_table_row(cols: &[String]) -> String {
86− format!("| {} |\n", cols.join(" | "))
87− }
88−
89− fn md_mono(s: &str) -> String {
90− format!("`{}`", s)
91− }
92−
93− pub fn typable_commands() -> Result<String, DynError> {
94− let mut md = String::new();
95− md.push_str(&md_table_heading(&[
96− "Name".to_owned(),
97− "Description".to_owned(),
98− ]));
99−
100− let cmdify = |s: &str| format!("`:{}`", s);
101−
102− for cmd in TYPABLE_COMMAND_LIST {
103− let names = std::iter::once(&cmd.name)
104− .chain(cmd.aliases.iter())
105− .map(|a| cmdify(a))
106− .collect::<Vec<_>>()
107− .join(", ");
108−
109− let doc = cmd.doc.replace('\n', "<br>");
110−
111− md.push_str(&md_table_row(&[names.to_owned(), doc.to_owned()]));
112− }
113−
114− Ok(md)
115− }
116−
117− pub fn lang_features() -> Result<String, DynError> {
118− let mut md = String::new();
119− let ts_features = TsFeature::all();
120−
121− let mut cols = vec!["Language".to_owned()];
122− cols.append(
123− &mut ts_features
124− .iter()
125− .map(|t| t.long_title().to_string())
126− .collect::<Vec<_>>(),
127− );
128− cols.push("Default LSP".to_owned());
129−
130− md.push_str(&md_table_heading(&cols));
131− let config = helpers::lang_config();
132−
133− let mut langs = config
134− .language
135− .iter()
136− .map(|l| l.language_id.clone())
137− .collect::<Vec<_>>();
138− langs.sort_unstable();
139−
140− let mut ts_features_to_langs = Vec::new();
141− for &feat in ts_features {
142− ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
143− }
144−
145− let mut row = Vec::new();
146− for lang in langs {
147− let lc = config
148− .language
149− .iter()
150− .find(|l| l.language_id == lang)
151− .unwrap(); // lang comes from config
152− row.push(lc.language_id.clone());
153−
154− for (_feat, support_list) in &ts_features_to_langs {
155− row.push(
156− if support_list.contains(&lang) {
157− "✓"
158− } else {
159− ""
160− }
161− .to_owned(),
162− );
163− }
164− row.push(
165− lc.language_server
166− .as_ref()
167− .map(|s| s.command.clone())
168− .map(|c| md_mono(&c))
169− .unwrap_or_default(),
170− );
171−
172− md.push_str(&md_table_row(&row));
173− row.clear();
174− }
175−
176− Ok(md)
177− }
178−
179− pub fn write(filename: &str, data: &str) {
180− let error = format!("Could not write to {}", filename);
181− let path = path::book_gen().join(filename);
182− fs::write(path, data).expect(&error);
183− }
184−}
185−
186−pub mod path {
187− use std::path::{Path, PathBuf};
188−
189− pub fn project_root() -> PathBuf {
190− Path::new(env!("CARGO_MANIFEST_DIR"))
191− .parent()
192− .unwrap()
193− .to_path_buf()
194− }
195−
196− pub fn book_gen() -> PathBuf {
197− project_root().join("book/src/generated/")
198− }
199−
200− pub fn ts_queries() -> PathBuf {
201− project_root().join("runtime/queries")
202− }
203−
204− pub fn lang_config() -> PathBuf {
205− project_root().join("languages.toml")
206− }
207−}
208−
20911pub mod tasks {
210− use crate::md_gen;
12+ use crate::docgen::{lang_features, typable_commands, write};
13+ use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT};
14+ use crate::querycheck::query_check;
15+ use crate::themelint::{lint, lint_all};
21116 use crate::DynError;
21217
21318 pub fn docgen() -> Result<(), DynError> {
214− use md_gen::*;
21519 write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
21620 write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
21721 Ok(())
21822 }
21923
220− pub fn query_check() -> Result<(), String> {
221− use crate::helpers::lang_config;
222− use helix_core::{syntax::read_query, tree_sitter::Query};
223− use helix_loader::grammar::get_language;
224−
225− let query_files = [
226− "highlights.scm",
227− "locals.scm",
228− "injections.scm",
229− "textobjects.scm",
230− "indents.scm",
231− ];
232−
233− for language in lang_config().language {
234− let language_name = language.language_id.to_ascii_lowercase();
235− let grammar_name = language.grammar.unwrap_or(language.language_id);
236− for query_file in query_files {
237− let language = get_language(&grammar_name);
238− let query_text = read_query(&language_name, query_file);
239− if !query_text.is_empty() && language.is_ok() {
240− if let Err(reason) = Query::new(language.unwrap(), &query_text) {
241− return Err(format!(
242− "Failed to parse {} queries for {}: {}",
243− query_file, language_name, reason
244− ));
245− }
246− }
247− }
24+ pub fn themelint(file: Option<String>) -> Result<(), DynError> {
25+ match file {
26+ Some(file) => lint(file),
27+ None => lint_all(),
24828 }
29+ }
24930
250− println!("Query check succeeded");
251−
252− Ok(())
31+ pub fn querycheck() -> Result<(), DynError> {
32+ query_check()
25333 }
25434
25535 pub fn print_help() {
@@ -259,6 +39,7 @@ Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
25939
26040 Tasks:
26141 docgen: Generate files to be included in the mdbook output.
42+ themelint <theme>: Report errors for <theme>, or all themes if no theme is specified.
26243 query-check: Check that tree-sitter queries are valid.
26344"
26445 );
@@ -271,7 +52,8 @@ fn main() -> Result<(), DynError> {
27152 None => tasks::print_help(),
27253 Some(t) => match t.as_str() {
27354 "docgen" => tasks::docgen()?,
274− "query-check" => tasks::query_check()?,
55+ "themelint" => tasks::themelint(env::args().nth(2))?,
56+ "query-check" => tasks::querycheck()?,
27557 invalid => return Err(format!("Invalid task name: {}", invalid).into()),
27658 },
27759 };
xtask/src/path.rsnew file+24
@@ -0,0 +1,24 @@
1+use std::path::{Path, PathBuf};
2+
3+pub fn project_root() -> PathBuf {
4+ Path::new(env!("CARGO_MANIFEST_DIR"))
5+ .parent()
6+ .unwrap()
7+ .to_path_buf()
8+}
9+
10+pub fn book_gen() -> PathBuf {
11+ project_root().join("book/src/generated/")
12+}
13+
14+pub fn ts_queries() -> PathBuf {
15+ project_root().join("runtime/queries")
16+}
17+
18+pub fn lang_config() -> PathBuf {
19+ project_root().join("languages.toml")
20+}
21+
22+pub fn themes() -> PathBuf {
23+ project_root().join("runtime/themes")
24+}
xtask/src/querycheck.rsnew file+39
@@ -0,0 +1,39 @@
1+use crate::DynError;
2+
3+pub fn query_check() -> Result<(), DynError> {
4+ use crate::helpers::lang_config;
5+ use helix_core::{syntax::read_query, tree_sitter::Query};
6+ use helix_loader::grammar::get_language;
7+
8+ let query_files = [
9+ "highlights.scm",
10+ "locals.scm",
11+ "injections.scm",
12+ "textobjects.scm",
13+ "indents.scm",
14+ ];
15+
16+ for language in lang_config().language {
17+ let language_name = language.language_id.to_ascii_lowercase();
18+ let grammar_name = language.grammar.unwrap_or(language.language_id);
19+ for query_file in query_files {
20+ let language = get_language(&grammar_name);
21+ let query_text = read_query(&language_name, query_file);
22+ if let Ok(lang) = language {
23+ if !query_text.is_empty() {
24+ if let Err(reason) = Query::new(lang, &query_text) {
25+ return Err(format!(
26+ "Failed to parse {} queries for {}: {}",
27+ query_file, language_name, reason
28+ )
29+ .into());
30+ }
31+ }
32+ }
33+ }
34+ }
35+
36+ println!("Query check succeeded");
37+
38+ Ok(())
39+}
xtask/src/themelint.rsnew file+199
@@ -0,0 +1,199 @@
1+use crate::path;
2+use crate::DynError;
3+use helix_view::theme::Loader;
4+use helix_view::theme::Modifier;
5+use helix_view::Theme;
6+
7+struct Rule {
8+ fg: Option<&'static str>,
9+ bg: Option<&'static str>,
10+ check_both: bool,
11+}
12+
13+enum Require {
14+ Existence(Rule),
15+ Difference(&'static str, &'static str),
16+}
17+
18+// Placed in an fn here, so it's the first thing you see
19+fn get_rules() -> Vec<Require> {
20+ vec![
21+ // Check for ui.selection, which is required
22+ Require::Existence(Rule::has_either("ui.selection")),
23+ Require::Existence(Rule::has_either("ui.selection.primary")),
24+ Require::Difference("ui.selection", "ui.selection.primary"),
25+ // Check for planned readable text
26+ Require::Existence(Rule::has_fg("ui.text")),
27+ Require::Existence(Rule::has_bg("ui.background")),
28+ // Check for complete editor.statusline bare minimum
29+ Require::Existence(Rule::has_both("ui.statusline")),
30+ Require::Existence(Rule::has_both("ui.statusline.inactive")),
31+ // Check for editor.color-modes
32+ Require::Existence(Rule::has_either("ui.statusline.normal")),
33+ Require::Existence(Rule::has_either("ui.statusline.insert")),
34+ Require::Existence(Rule::has_either("ui.statusline.select")),
35+ Require::Difference("ui.statusline.normal", "ui.statusline.insert"),
36+ Require::Difference("ui.statusline.normal", "ui.statusline.select"),
37+ // Check for editor.cursorline
38+ Require::Existence(Rule::has_bg("ui.cursorline.primary")),
39+ // Check for editor.whitespace
40+ Require::Existence(Rule::has_fg("ui.virtual.whitespace")),
41+ // Check fir rulers
42+ Require::Existence(Rule::has_either("ui.virtual.indent-guide")),
43+ // Check for editor.rulers
44+ Require::Existence(Rule::has_either("ui.virtual.ruler")),
45+ // Check for menus and prompts
46+ Require::Existence(Rule::has_both("ui.menu")),
47+ Require::Existence(Rule::has_both("ui.help")),
48+ Require::Existence(Rule::has_bg("ui.popup")),
49+ Require::Existence(Rule::has_either("ui.window")),
50+ // Check for visible cursor
51+ Require::Existence(Rule::has_bg("ui.cursor.primary")),
52+ Require::Existence(Rule::has_either("ui.cursor.match")),
53+ ]
54+}
55+
56+impl Rule {
57+ fn has_bg(bg: &'static str) -> Rule {
58+ Rule {
59+ fg: None,
60+ bg: Some(bg),
61+ check_both: true,
62+ }
63+ }
64+ fn has_fg(fg: &'static str) -> Rule {
65+ Rule {
66+ fg: Some(fg),
67+ bg: None,
68+ check_both: true,
69+ }
70+ }
71+ fn has_either(item: &'static str) -> Rule {
72+ Rule {
73+ fg: Some(item),
74+ bg: Some(item),
75+ check_both: false,
76+ }
77+ }
78+ fn has_both(item: &'static str) -> Rule {
79+ Rule {
80+ fg: Some(item),
81+ bg: Some(item),
82+ check_both: true,
83+ }
84+ }
85+ fn found_fg(&self, theme: &Theme) -> bool {
86+ if let Some(fg) = &self.fg {
87+ if theme.get(fg).fg.is_none() && theme.get(fg).add_modifier == Modifier::empty() {
88+ return false;
89+ }
90+ }
91+ true
92+ }
93+ fn found_bg(&self, theme: &Theme) -> bool {
94+ if let Some(bg) = &self.bg {
95+ if theme.get(bg).bg.is_none() && theme.get(bg).add_modifier == Modifier::empty() {
96+ return false;
97+ }
98+ }
99+ true
100+ }
101+ fn rule_name(&self) -> &'static str {
102+ if self.fg.is_some() {
103+ self.fg.unwrap()
104+ } else if self.bg.is_some() {
105+ self.bg.unwrap()
106+ } else {
107+ "LINTER_ERROR_NO_RULE"
108+ }
109+ }
110+
111+ fn check_difference(
112+ theme: &Theme,
113+ a: &'static str,
114+ b: &'static str,
115+ messages: &mut Vec<String>,
116+ ) {
117+ let theme_a = theme.get(a);
118+ let theme_b = theme.get(b);
119+ if theme_a == theme_b {
120+ messages.push(format!("$THEME: `{}` and `{}` cannot be equal", a, b));
121+ }
122+ }
123+
124+ fn check_existence(rule: &Rule, theme: &Theme, messages: &mut Vec<String>) {
125+ let found_fg = rule.found_fg(theme);
126+ let found_bg = rule.found_bg(theme);
127+
128+ if !rule.check_both && (found_fg || found_bg) {
129+ return;
130+ }
131+ if !found_fg || !found_bg {
132+ let mut missing = vec![];
133+ if !found_fg {
134+ missing.push("`fg`");
135+ }
136+ if !found_bg {
137+ missing.push("`bg`");
138+ }
139+ let entry = if !rule.check_both && !found_fg && !found_bg {
140+ missing.join(" or ")
141+ } else {
142+ missing.join(" and ")
143+ };
144+ messages.push(format!(
145+ "$THEME: missing {} for `{}`",
146+ entry,
147+ rule.rule_name()
148+ ))
149+ }
150+ }
151+}
152+
153+pub fn lint(file: String) -> Result<(), DynError> {
154+ if file.contains("base16") {
155+ println!("Skipping base16: {}", file);
156+ return Ok(());
157+ }
158+ let path = path::themes().join(file.clone() + ".toml");
159+ let theme = std::fs::read(&path).unwrap();
160+ let theme: Theme = toml::from_slice(&theme).expect("Failed to parse theme");
161+
162+ let mut messages: Vec<String> = vec![];
163+ get_rules().iter().for_each(|lint| match lint {
164+ Require::Existence(rule) => Rule::check_existence(rule, &theme, &mut messages),
165+ Require::Difference(a, b) => Rule::check_difference(&theme, a, b, &mut messages),
166+ });
167+
168+ if !messages.is_empty() {
169+ messages.iter().for_each(|m| {
170+ let theme = file.clone();
171+ let message = m.replace("$THEME", theme.as_str());
172+ println!("{}", message);
173+ });
174+ Err(format!("{} has issues", file.clone().as_str()).into())
175+ } else {
176+ Ok(())
177+ }
178+}
179+
180+pub fn lint_all() -> Result<(), DynError> {
181+ let files = Loader::read_names(path::themes().as_path());
182+ let files_count = files.len();
183+ let ok_files_count = files
184+ .into_iter()
185+ .filter_map(|path| lint(path.replace(".toml", "")).ok())
186+ .collect::<Vec<()>>()
187+ .len();
188+
189+ if files_count != ok_files_count {
190+ Err(format!(
191+ "{} of {} themes had issues",
192+ files_count - ok_files_count,
193+ files_count
194+ )
195+ .into())
196+ } else {
197+ Ok(())
198+ }
199+}