pub mod checks;
pub mod context;
pub mod imp;
pub mod inspect;
pub mod markedjson;
pub mod selfcheck;
pub mod spec;
use crate::ported::lint::checks::register_common_name;
use crate::ported::lint::markedjson::load;
use regex::Regex;
use serde_json::{Map, Value};
use std::sync::OnceLock;
pub fn open_file(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
}
pub fn function_name_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"^(\w+\.)*[a-zA-Z_]\w*$").unwrap())
}
pub fn register_common_names() {
register_common_name("player", "powerline.segments.common.players", "_player");
}
#[derive(Debug, Clone)]
pub struct LoadJsonResult {
pub hadproblem: bool,
pub config: Option<Value>,
pub error: Option<String>,
}
pub fn load_json_file(path: &std::path::Path) -> LoadJsonResult {
if !path.exists() {
return LoadJsonResult {
hadproblem: true,
config: None,
error: Some(format!("Path not found: {}", path.display())),
};
}
let (config, hadproblem) = load(path);
LoadJsonResult {
hadproblem,
config,
error: None,
}
}
pub fn updated_with_config(d: &mut Map<String, Value>) {
let path = d
.get("path")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from);
let path = match path {
Some(p) => p,
None => return,
};
let r = load_json_file(&path);
d.insert("hadproblem".to_string(), Value::Bool(r.hadproblem));
if let Some(cfg) = r.config {
d.insert("config".to_string(), cfg);
}
if let Some(err) = r.error {
d.insert("error".to_string(), Value::String(err));
}
}
pub fn dict2(d: &Map<String, Value>) -> Map<String, Value> {
let mut out = Map::new();
for (k, v) in d {
if let Some(inner) = v.as_object() {
out.insert(k.clone(), Value::Object(inner.clone()));
} else {
out.insert(k.clone(), v.clone());
}
}
out
}
pub fn strip_json_suffix(name: &str) -> String {
name.strip_suffix(".json").unwrap_or(name).to_string()
}
pub fn generate_json_config_loader(
lhadproblem: std::sync::Arc<std::sync::Mutex<bool>>,
) -> Box<dyn Fn(&std::path::Path) -> Option<Value>> {
Box::new(move |config_file_path: &std::path::Path| -> Option<Value> {
let (r, hadproblem) = load(config_file_path);
if hadproblem {
let mut flag = lhadproblem.lock().unwrap_or_else(|e| e.into_inner());
*flag = true;
}
r
})
}
#[derive(Debug, Clone)]
pub struct ExtConfigEntry {
pub error: Option<String>,
pub path: std::path::PathBuf,
pub name: Option<String>,
pub ext: Option<String>,
pub kind: Option<String>,
}
pub fn find_all_ext_config_files(
search_paths: &[std::path::PathBuf],
subdir: &str,
) -> Vec<ExtConfigEntry> {
let mut out: Vec<ExtConfigEntry> = Vec::new();
for config_root in search_paths {
let top_config_subpath = config_root.join(subdir);
if !top_config_subpath.is_dir() {
if top_config_subpath.exists() {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a directory",
top_config_subpath.display()
)),
path: top_config_subpath.clone(),
name: None,
ext: None,
kind: None,
});
}
continue;
}
let entries = match std::fs::read_dir(&top_config_subpath) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let ext_name = match entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
let ext_path = entry.path();
if !ext_path.is_dir() {
if ext_name.ends_with(".json") && ext_path.is_file() {
out.push(ExtConfigEntry {
error: None,
path: ext_path.clone(),
name: Some(strip_json_suffix(&ext_name)),
ext: None,
kind: Some(format!("top_{}", subdir)),
});
} else {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a directory or configuration file",
ext_path.display()
)),
path: ext_path.clone(),
name: None,
ext: None,
kind: None,
});
}
continue;
}
let inner = match std::fs::read_dir(&ext_path) {
Ok(e) => e,
Err(_) => continue,
};
for cfg_entry in inner.flatten() {
let config_file_name = match cfg_entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
let config_file_path = cfg_entry.path();
if config_file_name.ends_with(".json") && config_file_path.is_file() {
out.push(ExtConfigEntry {
error: None,
path: config_file_path.clone(),
name: Some(strip_json_suffix(&config_file_name)),
ext: Some(ext_name.clone()),
kind: Some(subdir.to_string()),
});
} else {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a configuration file",
config_file_path.display()
)),
path: config_file_path.clone(),
name: None,
ext: None,
kind: None,
});
}
}
}
}
out
}
pub fn check() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp_dir() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"powerliners-lint-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn function_name_re_matches_simple_name() {
let r = function_name_re();
assert!(r.is_match("foo"));
assert!(r.is_match("_private"));
assert!(r.is_match("func2"));
}
#[test]
fn function_name_re_matches_dotted_path() {
let r = function_name_re();
assert!(r.is_match("powerline.segments.common.sys.uptime"));
assert!(r.is_match("mymodule.fn"));
}
#[test]
fn function_name_re_rejects_starting_digit() {
let r = function_name_re();
assert!(!r.is_match("1foo"));
assert!(!r.is_match("powerline.1foo"));
}
#[test]
fn function_name_re_rejects_special_chars() {
let r = function_name_re();
assert!(!r.is_match("foo bar"));
assert!(!r.is_match("foo-bar"));
}
#[test]
fn open_file_reads_bytes() {
let d = tmp_dir();
let p = d.join("test.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"k\": 1}").unwrap();
let bytes = open_file(&p).unwrap();
assert_eq!(bytes, b"{\"k\": 1}".to_vec());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn open_file_missing_returns_error() {
let r = open_file(std::path::Path::new("/nonexistent/path/x.json"));
assert!(r.is_err());
}
#[test]
fn load_json_file_parses_valid_config() {
let d = tmp_dir();
let p = d.join("good.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"key\": \"value\"}").unwrap();
let r = load_json_file(&p);
assert!(!r.hadproblem);
assert!(r.config.is_some());
assert!(r.error.is_none());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn load_json_file_missing_returns_error() {
let r = load_json_file(std::path::Path::new("/never/exists/x.json"));
assert!(r.hadproblem);
assert!(r.config.is_none());
assert!(r.error.is_some());
}
#[test]
fn load_json_file_invalid_json_sets_hadproblem() {
let d = tmp_dir();
let p = d.join("bad.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"not valid json").unwrap();
let r = load_json_file(&p);
assert!(r.hadproblem);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn updated_with_config_merges_results_into_dict() {
let d_path = tmp_dir();
let p = d_path.join("c.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"a\": 1}").unwrap();
let mut d = Map::new();
d.insert(
"path".to_string(),
Value::String(p.to_string_lossy().to_string()),
);
updated_with_config(&mut d);
assert_eq!(d.get("hadproblem"), Some(&Value::Bool(false)));
assert!(d.get("config").is_some());
std::fs::remove_dir_all(&d_path).ok();
}
#[test]
fn updated_with_config_no_path_no_op() {
let mut d = Map::new();
d.insert("k".to_string(), Value::from(1));
updated_with_config(&mut d);
assert!(d.get("hadproblem").is_none());
assert!(d.get("config").is_none());
}
#[test]
fn dict2_shallow_copies_inner_dicts() {
let mut inner = Map::new();
inner.insert("inner_k".to_string(), Value::from(42));
let mut d = Map::new();
d.insert("outer".to_string(), Value::Object(inner));
let r = dict2(&d);
assert!(r.get("outer").is_some());
assert_eq!(
r["outer"].as_object().unwrap().get("inner_k"),
Some(&Value::from(42))
);
}
#[test]
fn dict2_passes_non_dict_values_through() {
let mut d = Map::new();
d.insert("scalar".to_string(), Value::from(7));
let r = dict2(&d);
assert_eq!(r["scalar"], Value::from(7));
}
#[test]
fn strip_json_suffix_removes_suffix() {
assert_eq!(strip_json_suffix("powerline.json"), "powerline");
assert_eq!(strip_json_suffix("default.json"), "default");
}
#[test]
fn strip_json_suffix_passes_through_when_absent() {
assert_eq!(strip_json_suffix("powerline"), "powerline");
assert_eq!(strip_json_suffix(""), "");
}
#[test]
fn load_json_result_struct_fields_accessible() {
let r = LoadJsonResult {
hadproblem: false,
config: Some(Value::Object(Map::new())),
error: None,
};
assert!(!r.hadproblem);
assert!(r.config.is_some());
assert!(r.error.is_none());
}
#[test]
fn register_common_names_inserts_player_alias() {
register_common_names();
let map = crate::ported::lint::checks::common_names()
.lock()
.unwrap_or_else(|e| e.into_inner());
assert!(map.contains_key("player"));
let entries = map.get("player").unwrap();
let pair = entries
.iter()
.find(|(m, n)| m == "powerline.segments.common.players" && n == "_player");
assert!(pair.is_some());
}
#[test]
fn generate_json_config_loader_loads_valid_config_and_flag_stays_false() {
let d = tmp_dir();
let p = d.join("good.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"k\": 1}").unwrap();
let flag = std::sync::Arc::new(std::sync::Mutex::new(false));
let loader = generate_json_config_loader(flag.clone());
let r = loader(&p);
assert!(r.is_some());
assert!(!*flag.lock().unwrap());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn generate_json_config_loader_missing_file_does_not_set_flag() {
let flag = std::sync::Arc::new(std::sync::Mutex::new(false));
let loader = generate_json_config_loader(flag.clone());
let r = loader(std::path::Path::new("/never/exists/x.json"));
assert!(r.is_none());
}
#[test]
fn find_all_ext_config_files_yields_ext_scoped_entry() {
let root = tmp_dir();
let themes_dir = root.join("themes");
let vim_dir = themes_dir.join("vim");
std::fs::create_dir_all(&vim_dir).unwrap();
std::fs::write(vim_dir.join("default.json"), "{}").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let ext_entry = entries
.iter()
.find(|e| e.error.is_none() && e.ext.is_some());
assert!(ext_entry.is_some());
let e = ext_entry.unwrap();
assert_eq!(e.name.as_deref(), Some("default"));
assert_eq!(e.ext.as_deref(), Some("vim"));
assert_eq!(e.kind.as_deref(), Some("themes"));
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_yields_top_level_entry() {
let root = tmp_dir();
let themes_dir = root.join("themes");
std::fs::create_dir_all(&themes_dir).unwrap();
std::fs::write(themes_dir.join("base.json"), "{}").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let top_entry = entries
.iter()
.find(|e| e.error.is_none() && e.kind.as_deref() == Some("top_themes"));
assert!(top_entry.is_some());
let e = top_entry.unwrap();
assert_eq!(e.name.as_deref(), Some("base"));
assert!(e.ext.is_none());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_yields_error_for_non_directory() {
let root = tmp_dir();
std::fs::write(root.join("themes"), "not-a-dir").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let err = entries
.iter()
.find(|e| e.error.as_deref().map(|s| s.contains("not a directory")) == Some(true));
assert!(err.is_some());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_skips_nonexistent_root_silently() {
let entries =
find_all_ext_config_files(&[std::path::PathBuf::from("/never/exists/abc")], "themes");
assert!(entries.is_empty());
}
#[test]
fn find_all_ext_config_files_yields_error_for_non_json_file_in_subdir() {
let root = tmp_dir();
let themes_dir = root.join("themes");
std::fs::create_dir_all(&themes_dir).unwrap();
std::fs::write(themes_dir.join("README.txt"), "not json").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let err = entries.iter().find(|e| {
e.error
.as_deref()
.map(|s| s.contains("not a directory or configuration file"))
== Some(true)
});
assert!(err.is_some());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_walks_multiple_search_paths() {
let r1 = tmp_dir();
let r2 = tmp_dir();
std::fs::create_dir_all(r1.join("themes").join("vim")).unwrap();
std::fs::create_dir_all(r2.join("themes").join("shell")).unwrap();
std::fs::write(r1.join("themes").join("vim").join("a.json"), "{}").unwrap();
std::fs::write(r2.join("themes").join("shell").join("b.json"), "{}").unwrap();
let entries = find_all_ext_config_files(&[r1.clone(), r2.clone()], "themes");
let exts: std::collections::HashSet<String> =
entries.iter().filter_map(|e| e.ext.clone()).collect();
assert!(exts.contains("vim"));
assert!(exts.contains("shell"));
std::fs::remove_dir_all(&r1).ok();
std::fs::remove_dir_all(&r2).ok();
}
}