1use crate::version::{version_matches_constraint, version_matches_normalized_constraint};
4use deps_core::ConcreteVersion;
5use deps_core::InvalidPackageName;
6use deps_core::PackageName;
7use deps_core::VersionReq;
8use deps_core::lsp_helpers::{
9 DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
10 RequirementMatcher, RequirementResolution, SourcePolicy,
11};
12use deps_core::normalize_operator_spacing;
13
14fn is_valid_dart_identifier(name: &str) -> bool {
18 let mut chars = name.chars();
19 chars
20 .next()
21 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
22 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
23}
24
25struct PubDevMatcher(String);
31
32impl RequirementMatcher for PubDevMatcher {
33 fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
34 let version = version.as_str();
35 Some(version_matches_normalized_constraint(version, &self.0))
36 }
37}
38
39pub struct DartFormatter;
40
41impl PackageNaming for DartFormatter {
42 fn validate_package_name(&self, name: &str) -> Result<(), InvalidPackageName> {
52 if name.is_empty() {
53 return Err(InvalidPackageName::new("name cannot be empty"));
54 }
55 if !is_valid_dart_identifier(name) {
56 return Err(InvalidPackageName::new(
57 "name must be a valid Dart identifier: only ASCII letters, digits, and '_', not starting with a digit",
58 ));
59 }
60 Ok(())
61 }
62}
63
64impl PackageRendering for DartFormatter {
65 fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
66 let version = version.as_str();
67 format!("^{version}")
68 }
69
70 fn package_url(&self, name: &PackageName) -> String {
71 crate::registry::package_url(name.as_str())
72 }
73}
74
75impl RequirementResolution for DartFormatter {
76 fn version_satisfies_requirement(&self, version: &ConcreteVersion, requirement: &str) -> bool {
77 let version = version.as_str();
78 version_matches_constraint(version, requirement)
79 }
80
81 fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
86 let normalized = normalize_operator_spacing(requirement.as_str().trim()).into_owned();
87 Some(Box::new(PubDevMatcher(normalized)))
88 }
89}
90
91impl DiagnosticMessages for DartFormatter {}
92
93impl DiagnosticPolicy for DartFormatter {}
94
95impl SourcePolicy for DartFormatter {}
96
97impl OsvNaming for DartFormatter {}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_format_version() {
105 let f = DartFormatter;
106 assert_eq!(
107 f.format_version_for_text_edit(&ConcreteVersion::new("1.0.0")),
108 "^1.0.0"
109 );
110 assert_eq!(
111 f.format_version_for_text_edit(&ConcreteVersion::new("6.1.2")),
112 "^6.1.2"
113 );
114 }
115
116 #[test]
117 fn test_package_url() {
118 let f = DartFormatter;
119 assert_eq!(
120 f.package_url(&PackageName::new("provider")),
121 "https://pub.dev/packages/provider"
122 );
123 }
124
125 #[test]
126 fn test_version_satisfies() {
127 let f = DartFormatter;
128 assert!(f.version_satisfies_requirement(&ConcreteVersion::new("1.5.0"), "^1.0.0"));
129 assert!(!f.version_satisfies_requirement(&ConcreteVersion::new("2.0.0"), "^1.0.0"));
130 }
131
132 #[test]
133 fn test_normalize_is_identity() {
134 let f = DartFormatter;
135 assert_eq!(
136 f.normalize_package_name(&PackageName::new("flutter_bloc")),
137 "flutter_bloc"
138 );
139 }
140
141 #[test]
142 fn test_compile_requirement_satisfiable() {
143 let f = DartFormatter;
144 let matcher = f
145 .compile_requirement(&VersionReq::new("^1.0.0"))
146 .expect("Dart requirement always compiles");
147 assert_eq!(matcher.matches(&ConcreteVersion::new("1.5.0")), Some(true));
148 assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(false));
149 }
150
151 #[test]
152 fn test_compile_requirement_spaced_range() {
153 let f = DartFormatter;
154 let matcher = f
155 .compile_requirement(&VersionReq::new(">= 1.15.0 < 2.0.0"))
156 .expect("Dart requirement always compiles");
157 assert_eq!(matcher.matches(&ConcreteVersion::new("1.15.0")), Some(true));
158 assert_eq!(matcher.matches(&ConcreteVersion::new("1.99.0")), Some(true));
159 assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(false));
160 assert_eq!(
161 matcher.matches(&ConcreteVersion::new("1.14.0")),
162 Some(false)
163 );
164 }
165
166 #[test]
167 fn test_validate_package_name_accepts_valid_names() {
168 let f = DartFormatter;
169 for name in ["provider", "flutter_bloc", "_private", "path9"] {
170 assert!(
171 f.validate_package_name(name).is_ok(),
172 "expected {name:?} to be accepted"
173 );
174 }
175 }
176
177 #[test]
181 fn test_validate_package_name_rejects_invalid_names() {
182 let f = DartFormatter;
183 for name in ["", "9path", "my-package", "my package", "日本語"] {
184 assert!(
185 f.validate_package_name(name).is_err(),
186 "expected {name:?} to be rejected"
187 );
188 }
189 }
190}