use lazy_static::lazy_static;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use super::rules::{Diagnostic, DiagnosticSeverity, Edit, Fix, Range, Rule, RuleCode};
use super::unused_opens::{analyze_opens, OpenStatement};
lazy_static! {
static ref OPEN_PATTERN: Regex = Regex::new(r"^open\s+([A-Z][\w.]*)").unwrap();
static ref INCLUDE_PATTERN: Regex = Regex::new(r"^include\s+([A-Z][\w.]*)").unwrap();
static ref QUALIFIED_USE_PATTERN: Regex = Regex::new(r"\b([A-Z][\w.]*)\.([\w']+)").unwrap();
static ref HEAVY_MODULES: HashSet<&'static str> = {
let mut set = HashSet::new();
set.insert("FStar.Tactics.V2");
set.insert("FStar.Tactics");
set.insert("FStar.Reflection.V2");
set.insert("FStar.Reflection");
set.insert("FStar.Tactics.V2.Derived");
set.insert("FStar.Tactics.V2.SyntaxCoercions");
set.insert("FStar.Tactics.CanonCommMonoid");
set.insert("FStar.Tactics.CanonCommSemiring");
set
};
static ref WHITELISTED_OPENS: HashSet<&'static str> = {
let mut set = HashSet::new();
set.insert("FStar.Mul"); set.insert("FStar.Pervasives"); set.insert("FStar.Pervasives.Native"); set.insert("FStar.HyperStack"); set.insert("FStar.HyperStack.ST"); set.insert("FStar.HyperStack.All"); set.insert("FStar.ST"); set.insert("FStar.Ghost"); set.insert("Lib.Buffer"); set.insert("Lib.IntTypes"); set.insert("Lib.Sequence"); set.insert("Lib.ByteBuffer"); set.insert("Lib.ByteSequence"); set.insert("Lib.LoopCombinators"); set.insert("Lib.IntVector"); set.insert("Lib.NTuple"); set.insert("Lib.MultiBuffer"); set.insert("LowStar.Buffer"); set.insert("LowStar.BufferOps"); set.insert("LowStar.Monotonic.Buffer"); set.insert("FStar.Math.Lemmas"); set.insert("FStar.Classical"); set.insert("FStar.Calc"); set
};
static ref TRANSITIVE_EXPORTS: HashMap<&'static str, Vec<&'static str>> = {
let mut map = HashMap::new();
map.insert("FStar.All", vec!["FStar.Exn", "FStar.ST"]);
map.insert("FStar.HyperStack.All", vec!["FStar.HyperStack.ST", "FStar.HyperStack"]);
map.insert("FStar.Tactics.V2", vec!["FStar.Tactics.V2.Derived", "FStar.Tactics.V2.SyntaxHelpers"]);
map
};
static ref KNOWN_EXPORTS: HashMap<&'static str, Vec<&'static str>> = {
let mut map = HashMap::new();
map.insert("FStar.List.Tot", vec![
"length", "hd", "tl", "nth", "index", "rev", "append",
"map", "mapi", "fold_left", "fold_right", "filter", "mem",
"for_all", "existsb", "find", "assoc", "split", "concatMap",
]);
map.insert("FStar.Seq", vec![
"seq", "length", "index", "create", "upd", "append", "slice",
"init", "head", "tail", "last", "cons", "snoc", "equal",
]);
map.insert("FStar.Set", vec![
"set", "empty", "singleton", "union", "intersect", "complement",
"mem", "equal", "subset", "disjoint",
]);
map.insert("FStar.Map", vec![
"map", "sel", "upd", "const", "concat", "restrict", "contains",
"domain", "equal",
]);
map.insert("FStar.UInt32", vec![
"t", "v", "uint_to_t", "add", "sub", "mul", "div",
"add_mod", "sub_mod", "mul_mod", "logand", "logor", "logxor",
]);
map.insert("FStar.UInt64", vec![
"t", "v", "uint_to_t", "add", "sub", "mul", "div",
"add_mod", "sub_mod", "mul_mod", "logand", "logor", "logxor",
]);
map
};
static ref SELECTIVE_THRESHOLD: usize = 3;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportCategory {
BroadImport,
UnusedImport,
PreferQualified,
CircularImport,
TransitiveImport,
HeavyModule,
}
impl ImportCategory {
pub fn code_suffix(&self) -> &'static str {
match self {
ImportCategory::BroadImport => "A",
ImportCategory::UnusedImport => "B",
ImportCategory::PreferQualified => "C",
ImportCategory::CircularImport => "D",
ImportCategory::TransitiveImport => "E",
ImportCategory::HeavyModule => "H",
}
}
}
#[derive(Debug, Default, Clone)]
struct ModuleUsage {
unqualified_names: HashSet<String>,
qualified_uses: usize,
open_line: Option<usize>,
}
#[derive(Debug)]
struct ImportAnalysis {
opens: Vec<OpenStatement>,
module_usage: HashMap<String, ModuleUsage>,
used_identifiers: HashSet<String>,
qualified_modules: HashSet<String>,
current_module: Option<String>,
}
fn analyze_imports(content: &str) -> ImportAnalysis {
let open_analysis = analyze_opens(content);
let mut module_usage: HashMap<String, ModuleUsage> = HashMap::new();
let current_module = extract_current_module(content);
for open in &open_analysis.opens {
let usage = module_usage.entry(open.module_path.clone()).or_default();
usage.open_line = Some(open.line);
}
let qualified_modules = extract_qualified_modules(content);
for module in &qualified_modules {
let usage = module_usage.entry(module.clone()).or_default();
usage.qualified_uses += 1;
}
for (module, exports) in KNOWN_EXPORTS.iter() {
let usage = module_usage.entry(module.to_string()).or_default();
for export in exports.iter() {
if open_analysis.used_identifiers.contains(*export) {
usage.unqualified_names.insert(export.to_string());
}
}
}
ImportAnalysis {
opens: open_analysis.opens,
module_usage,
used_identifiers: open_analysis.used_identifiers,
qualified_modules,
current_module,
}
}
fn extract_current_module(content: &str) -> Option<String> {
lazy_static! {
static ref MODULE_DECL: Regex = Regex::new(r"^module\s+([A-Z][\w.]*)\s*$").unwrap();
}
for line in content.lines() {
let trimmed = line.trim();
if let Some(caps) = MODULE_DECL.captures(trimmed) {
return Some(caps.get(1).unwrap().as_str().to_string());
}
}
None
}
fn extract_qualified_modules(content: &str) -> HashSet<String> {
let mut modules = HashSet::new();
let filtered: String = content
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.starts_with("open ")
&& !trimmed.starts_with("module ")
&& !trimmed.starts_with("friend ")
&& !trimmed.starts_with("include ")
})
.collect::<Vec<_>>()
.join("\n");
let clean = strip_comments_and_strings(&filtered);
for caps in QUALIFIED_USE_PATTERN.captures_iter(&clean) {
if let Some(m) = caps.get(1) {
modules.insert(m.as_str().to_string());
}
}
modules
}
fn strip_comments_and_strings(content: &str) -> String {
let mut result = String::with_capacity(content.len());
let chars: Vec<char> = content.chars().collect();
let n = chars.len();
let mut i = 0;
let mut comment_depth = 0;
let mut in_line_comment = false;
while i < n {
if i + 1 < n && chars[i] == '/' && chars[i + 1] == '/' {
in_line_comment = true;
i += 2;
continue;
}
if in_line_comment {
if chars[i] == '\n' {
in_line_comment = false;
result.push('\n');
}
i += 1;
continue;
}
if i + 1 < n && chars[i] == '(' && chars[i + 1] == '*' {
comment_depth += 1;
i += 2;
continue;
}
if i + 1 < n && chars[i] == '*' && chars[i + 1] == ')' && comment_depth > 0 {
comment_depth -= 1;
i += 2;
continue;
}
if comment_depth > 0 {
if chars[i] == '\n' {
result.push('\n');
}
i += 1;
continue;
}
if chars[i] == '"' {
i += 1;
while i < n && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < n {
i += 2;
} else {
i += 1;
}
}
if i < n {
i += 1;
}
continue;
}
result.push(chars[i]);
i += 1;
}
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircularImportType {
SelfImport,
ChildImportsParent,
ParentImportsChild,
None,
}
impl CircularImportType {
pub fn severity(&self) -> DiagnosticSeverity {
match self {
CircularImportType::SelfImport => DiagnosticSeverity::Error,
CircularImportType::ChildImportsParent => DiagnosticSeverity::Warning,
CircularImportType::ParentImportsChild => DiagnosticSeverity::Info,
CircularImportType::None => DiagnosticSeverity::Hint,
}
}
pub fn message(&self, current: &str, imported: &str) -> String {
match self {
CircularImportType::SelfImport => {
format!("[FST008-D] Self-import: module `{}` opens itself", current)
}
CircularImportType::ChildImportsParent => {
format!(
"[FST008-D] Parent import: module `{}` opens parent `{}` - may cause circular dependency",
current, imported
)
}
CircularImportType::ParentImportsChild => {
format!(
"[FST008-D] Child import: module `{}` opens child `{}` - unusual pattern, ensure no dependency cycle",
current, imported
)
}
CircularImportType::None => String::new(),
}
}
}
fn analyze_circular_import(current_module: &Option<String>, imported: &str) -> CircularImportType {
if let Some(current) = current_module {
if current == imported {
return CircularImportType::SelfImport;
}
if current.starts_with(&format!("{}.", imported)) {
return CircularImportType::ChildImportsParent;
}
if imported.starts_with(&format!("{}.", current)) {
return CircularImportType::ParentImportsChild;
}
}
CircularImportType::None
}
#[allow(dead_code)]
fn is_circular_import(current_module: &Option<String>, imported: &str) -> bool {
analyze_circular_import(current_module, imported) != CircularImportType::None
}
fn generate_selective_fix(
open: &OpenStatement,
used_names: &HashSet<String>,
file: &PathBuf,
) -> Option<Fix> {
if used_names.is_empty() {
return None;
}
let names: Vec<_> = used_names.iter().cloned().collect();
let selective_import = format!("open {} {{ {} }}", open.module_path, names.join(", "));
Some(Fix::new(
format!(
"Convert to selective import: only {} name{} used",
names.len(),
if names.len() == 1 { "" } else { "s" }
),
vec![Edit {
file: file.clone(),
range: Range::new(open.line, 1, open.line + 1, 1),
new_text: format!("{}\n", selective_import),
}],
))
}
fn generate_qualified_fix(open: &OpenStatement, file: &PathBuf) -> Fix {
Fix::new(
format!(
"Remove open and use qualified access: `{}.name`",
open.module_path
),
vec![Edit {
file: file.clone(),
range: Range::new(open.line, 1, open.line + 1, 1),
new_text: String::new(),
}],
)
}
pub struct ImportOptimizerRule {
selective_threshold: usize,
}
impl ImportOptimizerRule {
pub fn new() -> Self {
Self {
selective_threshold: *SELECTIVE_THRESHOLD,
}
}
pub fn with_threshold(threshold: usize) -> Self {
Self {
selective_threshold: threshold,
}
}
fn check_heavy_modules(&self, open: &OpenStatement, file: &PathBuf) -> Option<Diagnostic> {
if HEAVY_MODULES.contains(open.module_path.as_str()) {
Some(Diagnostic {
rule: RuleCode::FST008,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::new(
open.line,
open.col,
open.line,
open.col + open.line_text.trim().len(),
),
message: format!(
"[FST008-H] Heavy import: `{}` may significantly slow verification. \
Consider selective import or ensure it's necessary.",
open.module_path
),
fix: None,
})
} else {
None
}
}
fn check_broad_import(
&self,
open: &OpenStatement,
usage: &ModuleUsage,
file: &PathBuf,
) -> Option<Diagnostic> {
if open.selective.is_some() {
return None;
}
if WHITELISTED_OPENS.contains(open.module_path.as_str()) {
return None;
}
if let Some(_exports) = KNOWN_EXPORTS.get(open.module_path.as_str()) {
let used_count = usage.unqualified_names.len();
if used_count > 0 && used_count <= self.selective_threshold {
let fix = generate_selective_fix(open, &usage.unqualified_names, file);
return Some(Diagnostic {
rule: RuleCode::FST008,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::new(
open.line,
open.col,
open.line,
open.col + open.line_text.trim().len(),
),
message: format!(
"[FST008-A] Broad import: `open {}` but only {} name{} used ({}). \
Consider selective import.",
open.module_path,
used_count,
if used_count == 1 { "" } else { "s" },
usage
.unqualified_names
.iter()
.cloned()
.collect::<Vec<_>>()
.join(", ")
),
fix,
});
}
}
None
}
fn check_prefer_qualified(
&self,
open: &OpenStatement,
usage: &ModuleUsage,
file: &PathBuf,
) -> Option<Diagnostic> {
if WHITELISTED_OPENS.contains(open.module_path.as_str()) {
return None;
}
if !KNOWN_EXPORTS.contains_key(open.module_path.as_str()) {
return None;
}
if usage.qualified_uses > 0 && usage.unqualified_names.is_empty() {
return Some(Diagnostic {
rule: RuleCode::FST008,
severity: DiagnosticSeverity::Hint,
file: file.clone(),
range: Range::new(
open.line,
open.col,
open.line,
open.col + open.line_text.trim().len(),
),
message: format!(
"[FST008-C] Unnecessary open: `{}` is only accessed via qualified names. \
Consider removing the open statement.",
open.module_path
),
fix: Some(generate_qualified_fix(open, file)),
});
}
None
}
fn check_circular_import(
&self,
open: &OpenStatement,
current_module: &Option<String>,
file: &PathBuf,
) -> Option<Diagnostic> {
let circular_type = analyze_circular_import(current_module, &open.module_path);
if circular_type == CircularImportType::None
|| circular_type == CircularImportType::ParentImportsChild
{
return None;
}
let current = current_module.as_deref().unwrap_or("<unknown>");
Some(Diagnostic {
rule: RuleCode::FST008,
severity: circular_type.severity(),
file: file.clone(),
range: Range::new(
open.line,
open.col,
open.line,
open.col + open.line_text.trim().len(),
),
message: circular_type.message(current, &open.module_path),
fix: None,
})
}
fn check_transitive_import(
&self,
open: &OpenStatement,
analysis: &ImportAnalysis,
file: &PathBuf,
) -> Option<Diagnostic> {
if let Some(reexports) = TRANSITIVE_EXPORTS.get(open.module_path.as_str()) {
for reexported in reexports {
if analysis.opens.iter().any(|o| o.module_path == *reexported) {
return Some(Diagnostic {
rule: RuleCode::FST008,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::new(
open.line,
open.col,
open.line,
open.col + open.line_text.trim().len(),
),
message: format!(
"[FST008-E] Potentially redundant import: `{}` re-exports `{}`, \
which is also directly imported.",
open.module_path, reexported
),
fix: None,
});
}
}
}
None
}
}
impl Default for ImportOptimizerRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for ImportOptimizerRule {
fn code(&self) -> RuleCode {
RuleCode::FST008
}
fn check(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let analysis = analyze_imports(content);
let mut diagnostics = Vec::new();
for open in &analysis.opens {
let usage = analysis
.module_usage
.get(&open.module_path)
.cloned()
.unwrap_or_default();
if let Some(diag) = self.check_heavy_modules(open, file) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_circular_import(open, &analysis.current_module, file) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_transitive_import(open, &analysis, file) {
diagnostics.push(diag);
}
if !HEAVY_MODULES.contains(open.module_path.as_str()) {
if let Some(diag) = self.check_broad_import(open, &usage, file) {
diagnostics.push(diag);
}
}
if let Some(diag) = self.check_prefer_qualified(open, &usage, file) {
diagnostics.push(diag);
}
}
diagnostics
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heavy_module_detection() {
let content = r#"module Test
open FStar.Tactics.V2
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
assert!(!diagnostics.is_empty());
assert!(diagnostics[0].message.contains("FST008-H"));
assert!(diagnostics[0].message.contains("Heavy import"));
}
#[test]
fn test_broad_import_detection() {
let content = r#"module Test
open FStar.List.Tot
let x = length [1; 2; 3]
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let broad = diagnostics.iter().find(|d| d.message.contains("FST008-A"));
assert!(broad.is_some(), "Should detect broad import");
}
#[test]
fn test_circular_import_detection() {
let content = r#"module Test.Sub
open Test.Sub
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("Test/Sub.fst"), content);
let circular = diagnostics.iter().find(|d| d.message.contains("FST008-D"));
assert!(circular.is_some(), "Should detect circular import");
}
#[test]
fn test_selective_import_no_warning() {
let content = r#"module Test
open FStar.List.Tot { length }
let x = length [1; 2; 3]
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let broad = diagnostics.iter().find(|d| d.message.contains("FST008-A"));
assert!(
broad.is_none(),
"Selective import should not trigger broad import warning"
);
}
#[test]
fn test_strip_comments() {
let content = r#"
(* open FStar.Unused *)
let x = List.map (* comment *) f xs
// open Another.Unused
let y = 1
"#;
let stripped = strip_comments_and_strings(content);
assert!(!stripped.contains("FStar.Unused"));
assert!(!stripped.contains("Another.Unused"));
assert!(stripped.contains("List.map"));
}
#[test]
fn test_extract_current_module() {
let content = r#"module MyProject.Utils.Helper
open FStar.All
let helper x = x + 1
"#;
let module_name = extract_current_module(content);
assert_eq!(module_name, Some("MyProject.Utils.Helper".to_string()));
}
#[test]
fn test_parent_child_circular() {
let content = r#"module Test.Sub.Child
open Test.Sub
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let circular = diagnostics.iter().find(|d| d.message.contains("FST008-D"));
assert!(
circular.is_some(),
"Should detect parent-child circular dependency"
);
assert!(
circular.unwrap().message.contains("Parent import"),
"Should identify as parent import"
);
}
#[test]
fn test_self_import_detection() {
let content = r#"module MyModule
open MyModule
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let circular = diagnostics.iter().find(|d| d.message.contains("FST008-D"));
assert!(circular.is_some(), "Should detect self-import");
assert!(
circular.unwrap().message.contains("Self-import"),
"Should identify as self-import"
);
assert_eq!(
circular.unwrap().severity,
DiagnosticSeverity::Error,
"Self-import should be Error severity"
);
}
#[test]
fn test_parent_imports_child_suppressed() {
let content = r#"module Parent
open Parent.Child
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let circular = diagnostics.iter().find(|d| d.message.contains("FST008-D"));
assert!(
circular.is_none(),
"Parent importing child should NOT trigger FST008-D (normal F* pattern)"
);
}
#[test]
fn test_deep_parent_import() {
let content = r#"module A.B.C.D
open A.B
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let circular = diagnostics.iter().find(|d| d.message.contains("FST008-D"));
assert!(circular.is_some(), "Should detect deep parent import");
assert!(
circular.unwrap().message.contains("Parent import"),
"Should identify as parent import"
);
}
#[test]
fn test_no_fst008f_import_order() {
let content = r#"module Test
open MyProject.Utils
open FStar.Pervasives
open FStar.ST
open FStar.List.Tot
open FStar.Ghost
open FStar.Calc
open Hacl.Impl.SHA256
let x = 1
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let order_issues: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("FST008-F"))
.collect();
assert!(
order_issues.is_empty(),
"FST008-F import ordering should not be emitted"
);
}
#[test]
fn test_circular_import_type_messages() {
let self_msg = CircularImportType::SelfImport.message("A", "A");
assert!(self_msg.contains("Self-import"));
assert!(self_msg.contains("opens itself"));
let parent_msg = CircularImportType::ChildImportsParent.message("A.B", "A");
assert!(parent_msg.contains("Parent import"));
assert!(parent_msg.contains("may cause circular"));
let child_msg = CircularImportType::ParentImportsChild.message("A", "A.B");
assert!(child_msg.contains("Child import"));
assert!(child_msg.contains("unusual pattern"));
}
#[test]
fn test_analyze_circular_import() {
assert_eq!(
analyze_circular_import(&Some("Test".to_string()), "Test"),
CircularImportType::SelfImport
);
assert_eq!(
analyze_circular_import(&Some("A.B.C".to_string()), "A.B"),
CircularImportType::ChildImportsParent
);
assert_eq!(
analyze_circular_import(&Some("A.B".to_string()), "A"),
CircularImportType::ChildImportsParent
);
assert_eq!(
analyze_circular_import(&Some("A".to_string()), "A.B"),
CircularImportType::ParentImportsChild
);
assert_eq!(
analyze_circular_import(&Some("A.B".to_string()), "A.B.C"),
CircularImportType::ParentImportsChild
);
assert_eq!(
analyze_circular_import(&Some("A".to_string()), "B"),
CircularImportType::None
);
assert_eq!(
analyze_circular_import(&Some("A.B".to_string()), "C.D"),
CircularImportType::None
);
assert_eq!(
analyze_circular_import(&None, "A"),
CircularImportType::None
);
}
#[test]
fn test_no_false_positive_similar_names() {
assert_eq!(
analyze_circular_import(&Some("AModuleExtended".to_string()), "AModule"),
CircularImportType::None
);
assert_eq!(
analyze_circular_import(&Some("Testing.Sub".to_string()), "Test"),
CircularImportType::None
);
}
#[test]
fn test_whitelisted_module_no_broad_import_warning() {
let content = r#"module Test
open FStar.Mul
let x = 2 * 3
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let broad = diagnostics.iter().find(|d| d.message.contains("FST008-A"));
assert!(
broad.is_none(),
"Whitelisted module FStar.Mul should not trigger FST008-A"
);
}
#[test]
fn test_whitelisted_module_no_prefer_qualified_warning() {
let content = r#"module Test
open Lib.IntTypes
let x = Lib.IntTypes.Intrinsics.add_carry 0uy 1ul 2ul out
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let prefer_qual = diagnostics.iter().find(|d| d.message.contains("FST008-C"));
assert!(
prefer_qual.is_none(),
"Whitelisted module Lib.IntTypes should not trigger FST008-C"
);
}
#[test]
fn test_no_fst008c_for_unknown_modules() {
let content = r#"module Test
open Hacl.Bignum.Definitions
let x = Hacl.Bignum.Definitions.bn_v h b
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let prefer_qual = diagnostics.iter().find(|d| d.message.contains("FST008-C"));
assert!(
prefer_qual.is_none(),
"Unknown module should not trigger FST008-C"
);
}
#[test]
fn test_no_false_qualified_use_from_open_lines() {
let content = r#"module Test
open FStar.HyperStack
open FStar.HyperStack.ST
let f (h: mem) = h
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let prefer_qual = diagnostics.iter().find(|d| d.message.contains("FST008-C"));
assert!(
prefer_qual.is_none(),
"open FStar.HyperStack.ST should not make FStar.HyperStack appear as qualified use"
);
}
#[test]
fn test_extract_qualified_modules_filters_declarations() {
let content = r#"module Test
open FStar.HyperStack
open FStar.HyperStack.ST
module S = Hacl.Spec.Bignum
let x = S.bn_v h b
"#;
let modules = extract_qualified_modules(content);
assert!(
modules.contains("S"),
"Should find S as qualified module from `S.bn_v`"
);
assert!(
!modules.contains("FStar.HyperStack"),
"Should not extract FStar.HyperStack from `open FStar.HyperStack.ST` line"
);
assert!(
!modules.contains("Hacl.Spec"),
"Should not extract Hacl.Spec from module alias declaration"
);
}
#[test]
fn test_hacl_star_typical_file_no_false_positives() {
let content = r#"module Hacl.Bignum.Base
open FStar.HyperStack
open FStar.HyperStack.ST
open FStar.Mul
open Lib.IntTypes
open Lib.Buffer
open Hacl.Bignum.Definitions
module LSeq = Lib.Sequence
let addcarry_st c_in a b out =
Lib.IntTypes.Intrinsics.add_carry c_in a b out
let x = LSeq.index (as_seq h out) 0
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("Hacl/Bignum/Base.fst"), content);
let a_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("FST008-A"))
.collect();
let c_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("FST008-C"))
.collect();
assert!(
a_warnings.is_empty(),
"Typical HACL* file should not trigger FST008-A, got: {:?}",
a_warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
assert!(
c_warnings.is_empty(),
"Typical HACL* file should not trigger FST008-C, got: {:?}",
c_warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn test_fst008c_still_fires_for_known_exports_modules() {
let content = r#"module Test
open FStar.List.Tot
let x = FStar.List.Tot.length [1; 2; 3]
"#;
let rule = ImportOptimizerRule::new();
let diagnostics = rule.check(&PathBuf::from("test.fst"), content);
let prefer_qual = diagnostics.iter().find(|d| d.message.contains("FST008-C"));
assert!(
prefer_qual.is_some(),
"FST008-C should still fire for KNOWN_EXPORTS module used only qualified"
);
}
#[test]
fn test_whitelisted_modules_comprehensive() {
let critical_modules = vec![
"FStar.Mul",
"FStar.HyperStack",
"FStar.HyperStack.ST",
"Lib.IntTypes",
"Lib.Buffer",
"LowStar.Buffer",
"LowStar.BufferOps",
];
for module in critical_modules {
assert!(
WHITELISTED_OPENS.contains(module),
"Module {} should be in WHITELISTED_OPENS",
module
);
}
}
}