use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
use super::parser::{parse_fstar_file, BlockType};
use super::rules::{Diagnostic, DiagnosticSeverity, Range, Rule, RuleCode};
lazy_static! {
static ref LOWERCASE_START: Regex = Regex::new(r"^[a-z_][a-zA-Z0-9_]*$").unwrap();
static ref CAMEL_CASE: Regex = Regex::new(r"^[A-Z][a-zA-Z0-9]*$").unwrap();
static ref STARTS_UPPERCASE: Regex = Regex::new(r"^[A-Z]").unwrap();
}
fn is_lowercase_start(name: &str) -> bool {
LOWERCASE_START.is_match(name)
}
fn is_camel_case(name: &str) -> bool {
CAMEL_CASE.is_match(name)
}
fn starts_with_uppercase(name: &str) -> bool {
STARTS_UPPERCASE.is_match(name)
}
fn to_snake_case(name: &str) -> String {
let mut result = String::with_capacity(name.len() + 4);
let mut prev_was_upper = false;
let mut prev_was_underscore = true;
for (i, c) in name.chars().enumerate() {
if c.is_uppercase() {
if i > 0 && !prev_was_underscore {
let next_is_lower = name
.chars()
.nth(i + 1)
.map(|n| n.is_lowercase())
.unwrap_or(false);
if !prev_was_upper || next_is_lower {
result.push('_');
}
}
result.push(c.to_ascii_lowercase());
prev_was_upper = true;
prev_was_underscore = false;
} else if c == '_' {
result.push(c);
prev_was_upper = false;
prev_was_underscore = true;
} else {
result.push(c);
prev_was_upper = false;
prev_was_underscore = false;
}
}
result
}
pub struct NamingRule;
impl NamingRule {
pub fn new() -> Self {
Self
}
}
impl Default for NamingRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for NamingRule {
fn code(&self) -> RuleCode {
RuleCode::FST006
}
fn check(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let (_, blocks) = parse_fstar_file(content);
for block in &blocks {
for name in &block.names {
if name.starts_with('_') {
continue;
}
let check_name = name.trim_end_matches('\'');
if check_name.is_empty() {
continue;
}
match block.block_type {
BlockType::Type => {
if starts_with_uppercase(check_name) {
let suggested = to_snake_case(check_name);
diagnostics.push(Diagnostic {
rule: RuleCode::FST006,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(block.start_line, 1),
message: format!(
"Type `{}` should start with lowercase. \
PascalCase is reserved for constructors/modules. \
Suggested: `{}`",
name, suggested
),
fix: None,
});
}
}
BlockType::Let
| BlockType::Val
| BlockType::UnfoldLet
| BlockType::InlineLet => {
if starts_with_uppercase(check_name) {
let suggested = to_snake_case(check_name);
diagnostics.push(Diagnostic {
rule: RuleCode::FST006,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(block.start_line, 1),
message: format!(
"Function/value `{}` should start with lowercase. \
PascalCase is reserved for constructors/modules. \
Suggested: `{}`",
name, suggested
),
fix: None,
});
}
}
BlockType::Effect => {
if !starts_with_uppercase(check_name) {
diagnostics.push(Diagnostic {
rule: RuleCode::FST006,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(block.start_line, 1),
message: format!(
"Effect `{}` should use CamelCase (start with uppercase)",
name
),
fix: None,
});
}
}
_ => {}
}
}
}
diagnostics
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_lowercase_start() {
assert!(is_lowercase_start("foo"));
assert!(is_lowercase_start("foo_bar"));
assert!(is_lowercase_start("foo_bar_baz"));
assert!(is_lowercase_start("_internal"));
assert!(is_lowercase_start("int_t"));
assert!(is_lowercase_start("nat"));
assert!(is_lowercase_start("fooBar"));
assert!(is_lowercase_start("loadState"));
assert!(is_lowercase_start("createL"));
assert!(is_lowercase_start("mapT"));
assert!(is_lowercase_start("storeState_inner"));
assert!(is_lowercase_start("ivTable_S"));
assert!(is_lowercase_start("modBits_t"));
assert!(!is_lowercase_start("Foo"));
assert!(!is_lowercase_start("FooBar"));
assert!(!is_lowercase_start("FOO_BAR"));
}
#[test]
fn test_starts_with_uppercase() {
assert!(starts_with_uppercase("Foo"));
assert!(starts_with_uppercase("FooBar"));
assert!(starts_with_uppercase("Tot"));
assert!(starts_with_uppercase("GTot"));
assert!(starts_with_uppercase("FOO_BAR"));
assert!(!starts_with_uppercase("foo"));
assert!(!starts_with_uppercase("fooBar"));
assert!(!starts_with_uppercase("_foo"));
}
#[test]
fn test_is_camel_case() {
assert!(is_camel_case("Foo"));
assert!(is_camel_case("FooBar"));
assert!(is_camel_case("Tot"));
assert!(is_camel_case("GTot"));
assert!(is_camel_case("Lemma"));
assert!(!is_camel_case("foo"));
assert!(!is_camel_case("foo_bar"));
assert!(!is_camel_case("fooBar"));
}
#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("BadType"), "bad_type");
assert_eq!(to_snake_case("FooBar"), "foo_bar");
assert_eq!(to_snake_case("foo"), "foo");
assert_eq!(to_snake_case("XMLParser"), "xml_parser");
}
#[test]
fn test_naming_rule_type_pascal_case_flagged() {
let rule = NamingRule::new();
let content = r#"module Test
type BadType = int
type good_type = int
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("BadType"));
assert!(diagnostics[0].message.contains("lowercase"));
}
#[test]
fn test_naming_rule_type_camelcase_accepted() {
let rule = NamingRule::new();
let content = r#"module Test
type inttype = int
type range_t = int
type secrecy_level = int
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_naming_rule_function_camelcase_accepted() {
let rule = NamingRule::new();
let content = r#"module Test
val loadState : int -> int
let loadState x = x
val createL : int -> int
let createL x = x
val storeState_inner : int -> int
let storeState_inner x = x
val good_func : int -> int
let good_func x = x
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for camelCase functions, got: {:?}",
diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn test_naming_rule_function_pascal_case_flagged() {
let rule = NamingRule::new();
let content = r#"module Test
val BadFunc : int -> int
let BadFunc x = x
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert_eq!(diagnostics.len(), 2);
assert!(diagnostics[0].message.contains("BadFunc"));
assert!(diagnostics[0].message.contains("lowercase"));
}
#[test]
fn test_naming_rule_effect() {
let rule = NamingRule::new();
let content = r#"module Test
effect bad_effect = Tot
effect GoodEffect = Tot
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("bad_effect"));
assert!(diagnostics[0].message.contains("CamelCase"));
}
#[test]
fn test_naming_rule_underscore_prefix() {
let rule = NamingRule::new();
let content = r#"module Test
val _internal : int -> int
let _internal x = x
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_naming_rule_primed_names() {
let rule = NamingRule::new();
let content = r#"module Test
val foo' : int -> int
let foo' x = x
val loadState' : int -> int
let loadState' x = x
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for primed names, got: {:?}",
diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn test_naming_rule_hacl_star_patterns() {
let rule = NamingRule::new();
let content = r#"module Test
val sigmaTable : int -> int
let sigmaTable x = x
val ivTable_S : int -> int
let ivTable_S x = x
val rTable_B : int -> int
let rTable_B x = x
val msgHash2 : int -> int
let msgHash2 x = x
val modBits_t : int -> int
let modBits_t x = x
inline_for_extraction let fillT x = x
unfold let createL x = x
"#;
let file = PathBuf::from("test.fst");
let diagnostics = rule.check(&file, content);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for hacl-star patterns, got: {:?}",
diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
}