blob: fc6469d2f6e78e88f1ac1798e5deeb098b161705 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001//! Bazel label parsing library.
2//!
3//! USAGE: `label::analyze("//foo/bar:baz")
4mod label_error;
5use label_error::LabelError;
6
7/// Parse and analyze given str.
8///
9/// TODO: validate . and .. in target name
10/// TODO: validate used characters in target name
11pub fn analyze(input: &'_ str) -> Result<Label<'_>> {
12 let label = input;
13 let (input, repository_name) = consume_repository_name(input, label)?;
14 let (input, package_name) = consume_package_name(input, label)?;
15 let name = consume_name(input, label)?;
16 let name = match (package_name, name) {
17 (None, None) => {
18 return Err(LabelError(err(
19 label,
20 "labels must have a package and/or a name.",
21 )))
22 }
23 (Some(package_name), None) => name_from_package(package_name),
24 (_, Some(name)) => name,
25 };
26 Ok(Label::new(repository_name, package_name, name))
27}
28
Adam Snaider1c095c92023-07-08 02:09:58 -040029#[derive(Debug, PartialEq, Eq)]
Brian Silvermancc09f182022-03-09 15:40:20 -080030pub struct Label<'s> {
31 pub repository_name: Option<&'s str>,
32 pub package_name: Option<&'s str>,
33 pub name: &'s str,
34}
35
36type Result<T, E = LabelError> = core::result::Result<T, E>;
37
38impl<'s> Label<'s> {
39 fn new(
40 repository_name: Option<&'s str>,
41 package_name: Option<&'s str>,
42 name: &'s str,
43 ) -> Label<'s> {
44 Label {
45 repository_name,
46 package_name,
47 name,
48 }
49 }
50
51 pub fn packages(&self) -> Vec<&'s str> {
52 match self.package_name {
53 Some(name) => name.split('/').collect(),
54 None => vec![],
55 }
56 }
57}
58
59fn err<'s>(label: &'s str, msg: &'s str) -> String {
60 let mut err_msg = label.to_string();
61 err_msg.push_str(" must be a legal label; ");
62 err_msg.push_str(msg);
63 err_msg
64}
65
66fn consume_repository_name<'s>(
67 input: &'s str,
68 label: &'s str,
69) -> Result<(&'s str, Option<&'s str>)> {
70 if !input.starts_with('@') {
71 return Ok((input, None));
72 }
73
74 let slash_pos = input
75 .find("//")
76 .ok_or_else(|| err(label, "labels with repository must contain //."))?;
77 let repository_name = &input[1..slash_pos];
78 if repository_name.is_empty() {
79 return Ok((&input[1..], None));
80 }
81 if !repository_name
82 .chars()
83 .next()
84 .unwrap()
85 .is_ascii_alphabetic()
86 {
87 return Err(LabelError(err(
88 label,
89 "workspace names must start with a letter.",
90 )));
91 }
92 if !repository_name
93 .chars()
94 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
95 {
96 return Err(LabelError(err(
97 label,
98 "workspace names \
99 may contain only A-Z, a-z, 0-9, '-', '_', and '.'.",
100 )));
101 }
102 Ok((&input[slash_pos..], Some(repository_name)))
103}
104
105fn consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)> {
106 let is_absolute = match input.rfind("//") {
107 None => false,
108 Some(0) => true,
109 Some(_) => {
110 return Err(LabelError(err(
111 label,
112 "'//' cannot appear in the middle of the label.",
113 )));
114 }
115 };
116
117 let (package_name, rest) = match (is_absolute, input.find(':')) {
118 (false, colon_pos) if colon_pos.map_or(true, |pos| pos != 0) => {
119 return Err(LabelError(err(
120 label,
121 "relative packages are not permitted.",
122 )));
123 }
124 (_, colon_pos) => {
125 let (input, colon_pos) = if is_absolute {
126 (&input[2..], colon_pos.map(|cp| cp - 2))
127 } else {
128 (input, colon_pos)
129 };
130 match colon_pos {
131 Some(colon_pos) => (&input[0..colon_pos], &input[colon_pos..]),
132 None => (input, ""),
133 }
134 }
135 };
136
137 if package_name.is_empty() {
138 return Ok((rest, None));
139 }
140
141 if !package_name.chars().all(|c| {
142 c.is_ascii_alphanumeric()
143 || c == '/'
144 || c == '-'
145 || c == '.'
146 || c == ' '
147 || c == '$'
148 || c == '('
149 || c == ')'
150 || c == '_'
Adam Snaider1c095c92023-07-08 02:09:58 -0400151 || c == '+'
Brian Silvermancc09f182022-03-09 15:40:20 -0800152 }) {
153 return Err(LabelError(err(
154 label,
155 "package names may contain only A-Z, \
Adam Snaider1c095c92023-07-08 02:09:58 -0400156 a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')', '_', and '+'.",
Brian Silvermancc09f182022-03-09 15:40:20 -0800157 )));
158 }
159 if package_name.ends_with('/') {
160 return Err(LabelError(err(
161 label,
162 "package names may not end with '/'.",
163 )));
164 }
165
166 if rest.is_empty() && is_absolute {
167 // This label doesn't contain the target name, we have to use
168 // last segment of the package name as target name.
169 return Ok((
170 match package_name.rfind('/') {
171 Some(pos) => &package_name[pos..],
172 None => package_name,
173 },
174 Some(package_name),
175 ));
176 }
177
178 Ok((rest, Some(package_name)))
179}
180
181fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
182 if input.is_empty() {
183 return Ok(None);
184 }
185 if input == ":" {
186 return Err(LabelError(err(label, "empty target name.")));
187 }
188 let name = input
189 .strip_prefix(':')
190 .or_else(|| input.strip_prefix('/'))
191 .unwrap_or(input);
192 if name.starts_with('/') {
193 return Err(LabelError(err(
194 label,
195 "target names may not start with '/'.",
196 )));
197 }
198 Ok(Some(name))
199}
200
201fn name_from_package(package_name: &str) -> &str {
202 package_name
203 .rsplit_once('/')
204 .map(|tup| tup.1)
205 .unwrap_or(package_name)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_new() {
214 assert_eq!(
215 Label::new(Some("repo"), Some("foo/bar"), "baz"),
216 Label {
217 repository_name: Some("repo"),
218 package_name: Some("foo/bar"),
219 name: "baz",
220 }
221 );
222 }
223
224 #[test]
225 fn test_repository_name_parsing() -> Result<()> {
226 assert_eq!(analyze("@repo//:foo")?.repository_name, Some("repo"));
227 assert_eq!(analyze("@//:foo")?.repository_name, None);
228 assert_eq!(analyze("//:foo")?.repository_name, None);
229 assert_eq!(analyze(":foo")?.repository_name, None);
230
231 assert_eq!(analyze("@repo//foo/bar")?.repository_name, Some("repo"));
232 assert_eq!(analyze("@//foo/bar")?.repository_name, None);
233 assert_eq!(analyze("//foo/bar")?.repository_name, None);
234 assert_eq!(
235 analyze("foo/bar"),
236 Err(LabelError(
237 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
238 ))
239 );
240
241 assert_eq!(analyze("@repo//foo")?.repository_name, Some("repo"));
242 assert_eq!(analyze("@//foo")?.repository_name, None);
243 assert_eq!(analyze("//foo")?.repository_name, None);
244 assert_eq!(
245 analyze("foo"),
246 Err(LabelError(
247 "foo must be a legal label; relative packages are not permitted.".to_string()
248 ))
249 );
250
251 assert_eq!(
252 analyze("@foo:bar"),
253 Err(LabelError(
254 "@foo:bar must be a legal label; labels with repository must contain //."
255 .to_string()
256 ))
257 );
258
259 assert_eq!(
260 analyze("@AZab0123456789_-.//:foo")?.repository_name,
261 Some("AZab0123456789_-.")
262 );
263 assert_eq!(
264 analyze("@42//:baz"),
265 Err(LabelError(
266 "@42//:baz must be a legal label; workspace names must \
267 start with a letter."
268 .to_string()
269 ))
270 );
271 assert_eq!(
272 analyze("@foo#//:baz"),
273 Err(LabelError(
274 "@foo#//:baz must be a legal label; workspace names \
275 may contain only A-Z, a-z, 0-9, '-', '_', and '.'."
276 .to_string()
277 ))
278 );
279 Ok(())
280 }
281 #[test]
282 fn test_package_name_parsing() -> Result<()> {
283 assert_eq!(analyze("//:baz/qux")?.package_name, None);
284 assert_eq!(analyze(":baz/qux")?.package_name, None);
285
286 assert_eq!(analyze("//foo:baz/qux")?.package_name, Some("foo"));
287 assert_eq!(analyze("//foo/bar:baz/qux")?.package_name, Some("foo/bar"));
288 assert_eq!(
289 analyze("foo:baz/qux"),
290 Err(LabelError(
291 "foo:baz/qux must be a legal label; relative packages are not permitted."
292 .to_string()
293 ))
294 );
295 assert_eq!(
296 analyze("foo/bar:baz/qux"),
297 Err(LabelError(
298 "foo/bar:baz/qux must be a legal label; relative packages are not permitted."
299 .to_string()
300 ))
301 );
302
303 assert_eq!(analyze("//foo")?.package_name, Some("foo"));
304
305 assert_eq!(
306 analyze("foo//bar"),
307 Err(LabelError(
308 "foo//bar must be a legal label; '//' cannot appear in the middle of the label."
309 .to_string()
310 ))
311 );
312 assert_eq!(
313 analyze("//foo//bar"),
314 Err(LabelError(
315 "//foo//bar must be a legal label; '//' cannot appear in the middle of the label."
316 .to_string()
317 ))
318 );
319 assert_eq!(
320 analyze("foo//bar:baz"),
321 Err(LabelError(
322 "foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
323 .to_string()
324 ))
325 );
326 assert_eq!(
327 analyze("//foo//bar:baz"),
328 Err(LabelError(
329 "//foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
330 .to_string()
331 ))
332 );
333
334 assert_eq!(
335 analyze("//azAZ09/-. $()_:baz")?.package_name,
336 Some("azAZ09/-. $()_")
337 );
338 assert_eq!(
339 analyze("//bar#:baz"),
340 Err(LabelError(
341 "//bar#:baz must be a legal label; package names may contain only A-Z, \
Adam Snaider1c095c92023-07-08 02:09:58 -0400342 a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')', '_', and '+'."
Brian Silvermancc09f182022-03-09 15:40:20 -0800343 .to_string()
344 ))
345 );
346 assert_eq!(
347 analyze("//bar/:baz"),
348 Err(LabelError(
349 "//bar/:baz must be a legal label; package names may not end with '/'.".to_string()
350 ))
351 );
352
353 assert_eq!(analyze("@repo//foo/bar")?.package_name, Some("foo/bar"));
354 assert_eq!(analyze("//foo/bar")?.package_name, Some("foo/bar"));
355 assert_eq!(
356 analyze("foo/bar"),
357 Err(LabelError(
358 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
359 ))
360 );
361
362 assert_eq!(analyze("@repo//foo")?.package_name, Some("foo"));
363 assert_eq!(analyze("//foo")?.package_name, Some("foo"));
364 assert_eq!(
365 analyze("foo"),
366 Err(LabelError(
367 "foo must be a legal label; relative packages are not permitted.".to_string()
368 ))
369 );
370
371 Ok(())
372 }
373
374 #[test]
375 fn test_name_parsing() -> Result<()> {
376 assert_eq!(analyze("//foo:baz")?.name, "baz");
377 assert_eq!(analyze("//foo:baz/qux")?.name, "baz/qux");
378
379 assert_eq!(
380 analyze("//bar:"),
381 Err(LabelError(
382 "//bar: must be a legal label; empty target name.".to_string()
383 ))
384 );
385 assert_eq!(analyze("//foo")?.name, "foo");
386
387 assert_eq!(
388 analyze("//bar:/foo"),
389 Err(LabelError(
390 "//bar:/foo must be a legal label; target names may not start with '/'."
391 .to_string()
392 ))
393 );
394
395 assert_eq!(analyze("@repo//foo/bar")?.name, "bar");
396 assert_eq!(analyze("//foo/bar")?.name, "bar");
397 assert_eq!(
398 analyze("foo/bar"),
399 Err(LabelError(
400 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
401 ))
402 );
403
404 assert_eq!(analyze("@repo//foo")?.name, "foo");
405 assert_eq!(analyze("//foo")?.name, "foo");
406 assert_eq!(
407 analyze("foo"),
408 Err(LabelError(
409 "foo must be a legal label; relative packages are not permitted.".to_string()
410 ))
411 );
412
413 Ok(())
414 }
415
416 #[test]
417 fn test_packages() -> Result<()> {
418 assert_eq!(analyze("@repo//:baz")?.packages(), Vec::<&str>::new());
419 assert_eq!(analyze("@repo//foo:baz")?.packages(), vec!["foo"]);
420 assert_eq!(
421 analyze("@repo//foo/bar:baz")?.packages(),
422 vec!["foo", "bar"]
423 );
424
Adam Snaider1c095c92023-07-08 02:09:58 -0400425 // Plus (+) is valid in packages
426 assert_eq!(
427 analyze("@repo//foo/bar+baz:qaz")?.packages(),
428 vec!["foo", "bar+baz"]
429 );
430
Brian Silvermancc09f182022-03-09 15:40:20 -0800431 Ok(())
432 }
433}