@@ -55,16 +55,21 @@ fn determine_semver_range(value: &str) -> Option<SemverRange> {
5555}
5656
5757/// Normalise values which are needlessly different
58- fn sanitise_value ( value : & str ) -> String {
58+ /// Returns None if no changes needed, Some(String) if modified
59+ fn sanitise_value ( value : & str ) -> Option < String > {
5960 if value == "latest" || value == "x" {
60- "*" . to_string ( )
61- } else {
62- let value = value. replace ( ".x" , "" ) . replace ( ".*" , "" ) ;
63- if value . starts_with ( "v" ) {
64- value . chars ( ) . skip ( 1 ) . collect ( )
61+ Some ( "*" . to_string ( ) )
62+ } else if value . contains ( ".x" ) || value . contains ( ".*" ) {
63+ let sanitised = value. replace ( ".x" , "" ) . replace ( ".*" , "" ) ;
64+ if sanitised . starts_with ( 'v' ) {
65+ Some ( sanitised . chars ( ) . skip ( 1 ) . collect ( ) )
6566 } else {
66- value
67+ Some ( sanitised )
6768 }
69+ } else if value. starts_with ( 'v' ) {
70+ Some ( value. chars ( ) . skip ( 1 ) . collect ( ) )
71+ } else {
72+ None
6873 }
6974}
7075
@@ -83,98 +88,146 @@ pub enum Specifier {
8388}
8489
8590impl Specifier {
86- /// Create a new instance
8791 pub fn new ( value : & str , local_version : Option < & BasicSemver > ) -> Self {
88- let raw = value. to_string ( ) ;
89-
90- if parser:: is_workspace_protocol ( value) {
91- return Self :: from_workspace_protocol ( value, local_version, raw) ;
92- } else if parser:: is_alias ( value) {
93- return Self :: from_alias ( value, raw) ;
94- } else if parser:: is_git ( value) {
95- return Self :: from_git ( value, raw) ;
96- } else if parser:: is_file ( value) {
97- return Self :: File ( raw:: Raw { raw } ) ;
98- } else if parser:: is_url ( value) {
99- return Self :: Url ( raw:: Raw { raw } ) ;
92+ let first_char = value. chars ( ) . next ( ) . unwrap_or ( '\0' ) ;
93+
94+ if first_char. is_ascii_digit ( )
95+ || first_char == '^'
96+ || first_char == '~'
97+ || first_char == '>'
98+ || first_char == '<'
99+ || first_char == '*'
100+ || ( first_char == 'l' && value == "latest" )
101+ || ( first_char == 'x' && value == "x" )
102+ {
103+ let sanitised = sanitise_value ( value) ;
104+ let sanitised_str = sanitised. as_deref ( ) . unwrap_or ( value) ;
105+ match Range :: parse ( sanitised_str) {
106+ Ok ( node_range) => {
107+ if parser:: is_complex_range ( sanitised_str) {
108+ return Self :: ComplexSemver ( ComplexSemver {
109+ raw : value. to_string ( ) ,
110+ node_range,
111+ } ) ;
112+ } else {
113+ match BasicSemver :: new ( sanitised_str) {
114+ Some ( semver) => return Self :: BasicSemver ( semver) ,
115+ None => return Self :: Unsupported ( raw:: Raw { raw : value. to_string ( ) } ) ,
116+ }
117+ }
118+ }
119+ Err ( _) => return Self :: Unsupported ( raw:: Raw { raw : value. to_string ( ) } ) ,
120+ }
121+ }
122+
123+ if first_char == 'w' && value. starts_with ( "workspace:" ) {
124+ return Self :: from_workspace_protocol ( value, local_version, value. to_string ( ) ) ;
125+ }
126+ if first_char == 'n' && value. starts_with ( "npm:" ) {
127+ return Self :: from_alias ( value, value. to_string ( ) ) ;
128+ }
129+ if first_char == 'g' && parser:: is_git ( value) {
130+ return Self :: from_git ( value, value. to_string ( ) ) ;
131+ }
132+ if first_char == 'f' && value. starts_with ( "file:" ) {
133+ return Self :: File ( raw:: Raw { raw : value. to_string ( ) } ) ;
134+ }
135+ if first_char == 'h' && ( value. starts_with ( "http://" ) || value. starts_with ( "https://" ) ) {
136+ return Self :: Url ( raw:: Raw { raw : value. to_string ( ) } ) ;
100137 }
101138
139+ // Handle remaining cases (tags, etc.)
102140 let sanitised = sanitise_value ( value) ;
103- let value = sanitised. as_str ( ) ;
141+ let sanitised_str = sanitised. as_deref ( ) . unwrap_or ( value ) ;
104142
105- if parser:: is_tag ( value) {
106- return Self :: Tag ( raw:: Raw { raw } ) ;
143+ // Check if it's a tag (alphabetic only)
144+ if parser:: is_tag ( sanitised_str) {
145+ return Self :: Tag ( raw:: Raw { raw : value. to_string ( ) } ) ;
107146 }
108147
109- match Range :: parse ( value) {
148+ // Final fallback - try to parse as semver range anyway
149+ match Range :: parse ( sanitised_str) {
110150 Ok ( node_range) => {
111- if parser:: is_complex_range ( value) {
112- Self :: ComplexSemver ( ComplexSemver { raw, node_range } )
151+ if parser:: is_complex_range ( sanitised_str) {
152+ Self :: ComplexSemver ( ComplexSemver {
153+ raw : value. to_string ( ) ,
154+ node_range,
155+ } )
113156 } else {
114- match BasicSemver :: new ( value ) {
157+ match BasicSemver :: new ( sanitised_str ) {
115158 Some ( semver) => Self :: BasicSemver ( semver) ,
116- None => Self :: Unsupported ( raw:: Raw { raw } ) ,
159+ None => Self :: Unsupported ( raw:: Raw { raw : value . to_string ( ) } ) ,
117160 }
118161 }
119162 }
120- Err ( _) => Self :: Unsupported ( raw:: Raw { raw } ) ,
163+ Err ( _) => Self :: Unsupported ( raw:: Raw { raw : value . to_string ( ) } ) ,
121164 }
122165 }
123166
124167 /// Create a new instance from a specifier containing "workspace:"
125168 fn from_workspace_protocol ( value : & str , local_version : Option < & BasicSemver > , raw : String ) -> Self {
126169 local_version
127170 . and_then ( |local| {
128- let without_protocol = value. replace ( "workspace:" , "" ) ;
129- let sanitised = sanitise_value ( & without_protocol) ;
130- if parser:: is_simple_semver ( & sanitised) {
171+ // Skip "workspace:" prefix (10 chars) to avoid allocation
172+ let without_protocol = & value[ 10 ..] ;
173+ let sanitised = sanitise_value ( without_protocol) ;
174+ let sanitised_str = sanitised. as_deref ( ) . unwrap_or ( without_protocol) ;
175+
176+ if parser:: is_simple_semver ( sanitised_str) {
131177 Some ( Self :: WorkspaceProtocol ( WorkspaceProtocol {
132- raw : format ! ( "workspace:{sanitised}" ) ,
178+ raw : if sanitised. is_some ( ) {
179+ format ! ( "workspace:{sanitised_str}" )
180+ } else {
181+ value. to_string ( )
182+ } ,
133183 local_version : local. clone ( ) ,
134- semver : BasicSemver :: new ( & sanitised ) . unwrap ( ) ,
184+ semver : BasicSemver :: new ( sanitised_str ) . unwrap ( ) ,
135185 } ) )
136- } else if sanitised == "~" || sanitised == "^" {
186+ } else if sanitised_str == "~" || sanitised_str == "^" {
187+ let combined_version = format ! ( "{}{}" , sanitised_str, local. raw) ;
137188 Some ( Self :: WorkspaceProtocol ( WorkspaceProtocol {
138- raw : format ! ( "workspace:{sanitised }" ) ,
189+ raw : format ! ( "workspace:{sanitised_str }" ) ,
139190 local_version : local. clone ( ) ,
140- semver : BasicSemver :: new ( & format ! ( "{}{}" , sanitised , local . raw ) ) . unwrap ( ) ,
191+ semver : BasicSemver :: new ( & combined_version ) . unwrap ( ) ,
141192 } ) )
142193 } else {
143194 None
144195 }
145196 } )
146- . unwrap_or_else ( || Self :: Unsupported ( raw:: Raw { raw : raw . clone ( ) } ) )
197+ . unwrap_or ( Self :: Unsupported ( raw:: Raw { raw } ) )
147198 }
148199
149200 /// Create a new instance from an npm alias specifier
150201 fn from_alias ( value : & str , raw : String ) -> Self {
151- let ( aliased_name, aliased_version) = {
152- let start = value. find ( ':' ) . unwrap ( ) + 1 ;
153- if let Some ( at_pos) = value. rfind ( '@' ) {
154- if at_pos > start {
155- // There's a version specifier
156- ( value[ start..at_pos] . to_string ( ) , value[ at_pos + 1 ..] . to_string ( ) )
157- } else {
158- // The @ is part of a scoped package name, no version
159- ( value[ start..] . to_string ( ) , String :: new ( ) )
160- }
202+ // Skip "npm:" prefix (4 chars) to avoid allocation
203+ let after_prefix = & value[ 4 ..] ;
204+
205+ let ( aliased_name, aliased_version) = if let Some ( at_pos) = after_prefix. rfind ( '@' ) {
206+ // Check if this @ is actually a version separator (not part of scoped name)
207+ if at_pos > 0 && !after_prefix[ ..at_pos] . is_empty ( ) {
208+ // There's a version specifier
209+ ( & after_prefix[ ..at_pos] , & after_prefix[ at_pos + 1 ..] )
161210 } else {
162- // No @ at all, unscoped package without version
163- ( value [ start.. ] . to_string ( ) , String :: new ( ) )
211+ // The @ is part of a scoped package name, no version
212+ ( after_prefix , "" )
164213 }
214+ } else {
215+ // No @ at all, unscoped package without version
216+ ( after_prefix, "" )
165217 } ;
218+
166219 if aliased_name. is_empty ( ) {
167220 Self :: Unsupported ( raw:: Raw { raw } )
168221 } else if aliased_version. is_empty ( ) {
169222 Self :: Alias ( alias:: Alias {
170223 raw,
171- name : aliased_name,
224+ name : aliased_name. to_string ( ) ,
172225 semver : None ,
173226 } )
174- } else if let Self :: BasicSemver ( inner) = Self :: new ( & aliased_version, None ) {
227+ } else if let Self :: BasicSemver ( inner) = Self :: new ( aliased_version, None ) {
175228 Self :: Alias ( alias:: Alias {
176229 raw,
177- name : aliased_name,
230+ name : aliased_name. to_string ( ) ,
178231 semver : Some ( inner) ,
179232 } )
180233 } else {
@@ -185,22 +238,44 @@ impl Specifier {
185238 /// Create a new instance from a git specifier, this can be a git url or some
186239 /// kind of github shorthand
187240 fn from_git ( value : & str , raw : String ) -> Self {
188- let parts = value. split ( '#' ) . collect :: < Vec < & str > > ( ) ;
189- let git_tag = parts. get ( 1 ) . map ( |tag| tag. to_string ( ) ) . unwrap_or_default ( ) ;
190- let git_tag = sanitise_value ( & git_tag) ;
191- let origin = parts. first ( ) . map ( |origin| origin. to_string ( ) ) . unwrap_or_default ( ) ;
192- if origin. is_empty ( ) {
193- Self :: Unsupported ( raw:: Raw { raw } )
194- } else if git_tag. is_empty ( ) {
195- Self :: Git ( git:: Git { raw, origin, semver : None } )
196- } else if let Some ( inner) = BasicSemver :: new ( & git_tag) {
241+ if let Some ( hash_pos) = value. find ( '#' ) {
242+ let origin = & value[ ..hash_pos] ;
243+ let git_tag_str = & value[ hash_pos + 1 ..] ;
244+
245+ if origin. is_empty ( ) {
246+ return Self :: Unsupported ( raw:: Raw { raw } ) ;
247+ }
248+
249+ if git_tag_str. is_empty ( ) {
250+ Self :: Git ( git:: Git {
251+ raw,
252+ origin : origin. to_string ( ) ,
253+ semver : None ,
254+ } )
255+ } else {
256+ let sanitised_tag = sanitise_value ( git_tag_str) ;
257+ let tag_str = sanitised_tag. as_deref ( ) . unwrap_or ( git_tag_str) ;
258+ if let Some ( inner) = BasicSemver :: new ( tag_str) {
259+ Self :: Git ( git:: Git {
260+ raw,
261+ origin : origin. to_string ( ) ,
262+ semver : Some ( inner) ,
263+ } )
264+ } else {
265+ Self :: Git ( git:: Git {
266+ raw,
267+ origin : origin. to_string ( ) ,
268+ semver : None ,
269+ } )
270+ }
271+ }
272+ } else {
273+ // No hash, just the origin
197274 Self :: Git ( git:: Git {
198275 raw,
199- origin,
200- semver : Some ( inner ) ,
276+ origin : value . to_string ( ) ,
277+ semver : None ,
201278 } )
202- } else {
203- Self :: Git ( git:: Git { raw, origin, semver : None } )
204279 }
205280 }
206281
0 commit comments