blob: 22d2bb4302c85bf79aa8bd393430849cfa94cd8a [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
29#[derive(Debug, PartialEq)]
30pub 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 == '_'
151 }) {
152 return Err(LabelError(err(
153 label,
154 "package names may contain only A-Z, \
155 a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'.",
156 )));
157 }
158 if package_name.ends_with('/') {
159 return Err(LabelError(err(
160 label,
161 "package names may not end with '/'.",
162 )));
163 }
164
165 if rest.is_empty() && is_absolute {
166 // This label doesn't contain the target name, we have to use
167 // last segment of the package name as target name.
168 return Ok((
169 match package_name.rfind('/') {
170 Some(pos) => &package_name[pos..],
171 None => package_name,
172 },
173 Some(package_name),
174 ));
175 }
176
177 Ok((rest, Some(package_name)))
178}
179
180fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
181 if input.is_empty() {
182 return Ok(None);
183 }
184 if input == ":" {
185 return Err(LabelError(err(label, "empty target name.")));
186 }
187 let name = input
188 .strip_prefix(':')
189 .or_else(|| input.strip_prefix('/'))
190 .unwrap_or(input);
191 if name.starts_with('/') {
192 return Err(LabelError(err(
193 label,
194 "target names may not start with '/'.",
195 )));
196 }
197 Ok(Some(name))
198}
199
200fn name_from_package(package_name: &str) -> &str {
201 package_name
202 .rsplit_once('/')
203 .map(|tup| tup.1)
204 .unwrap_or(package_name)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_new() {
213 assert_eq!(
214 Label::new(Some("repo"), Some("foo/bar"), "baz"),
215 Label {
216 repository_name: Some("repo"),
217 package_name: Some("foo/bar"),
218 name: "baz",
219 }
220 );
221 }
222
223 #[test]
224 fn test_repository_name_parsing() -> Result<()> {
225 assert_eq!(analyze("@repo//:foo")?.repository_name, Some("repo"));
226 assert_eq!(analyze("@//:foo")?.repository_name, None);
227 assert_eq!(analyze("//:foo")?.repository_name, None);
228 assert_eq!(analyze(":foo")?.repository_name, None);
229
230 assert_eq!(analyze("@repo//foo/bar")?.repository_name, Some("repo"));
231 assert_eq!(analyze("@//foo/bar")?.repository_name, None);
232 assert_eq!(analyze("//foo/bar")?.repository_name, None);
233 assert_eq!(
234 analyze("foo/bar"),
235 Err(LabelError(
236 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
237 ))
238 );
239
240 assert_eq!(analyze("@repo//foo")?.repository_name, Some("repo"));
241 assert_eq!(analyze("@//foo")?.repository_name, None);
242 assert_eq!(analyze("//foo")?.repository_name, None);
243 assert_eq!(
244 analyze("foo"),
245 Err(LabelError(
246 "foo must be a legal label; relative packages are not permitted.".to_string()
247 ))
248 );
249
250 assert_eq!(
251 analyze("@foo:bar"),
252 Err(LabelError(
253 "@foo:bar must be a legal label; labels with repository must contain //."
254 .to_string()
255 ))
256 );
257
258 assert_eq!(
259 analyze("@AZab0123456789_-.//:foo")?.repository_name,
260 Some("AZab0123456789_-.")
261 );
262 assert_eq!(
263 analyze("@42//:baz"),
264 Err(LabelError(
265 "@42//:baz must be a legal label; workspace names must \
266 start with a letter."
267 .to_string()
268 ))
269 );
270 assert_eq!(
271 analyze("@foo#//:baz"),
272 Err(LabelError(
273 "@foo#//:baz must be a legal label; workspace names \
274 may contain only A-Z, a-z, 0-9, '-', '_', and '.'."
275 .to_string()
276 ))
277 );
278 Ok(())
279 }
280 #[test]
281 fn test_package_name_parsing() -> Result<()> {
282 assert_eq!(analyze("//:baz/qux")?.package_name, None);
283 assert_eq!(analyze(":baz/qux")?.package_name, None);
284
285 assert_eq!(analyze("//foo:baz/qux")?.package_name, Some("foo"));
286 assert_eq!(analyze("//foo/bar:baz/qux")?.package_name, Some("foo/bar"));
287 assert_eq!(
288 analyze("foo:baz/qux"),
289 Err(LabelError(
290 "foo:baz/qux must be a legal label; relative packages are not permitted."
291 .to_string()
292 ))
293 );
294 assert_eq!(
295 analyze("foo/bar:baz/qux"),
296 Err(LabelError(
297 "foo/bar:baz/qux must be a legal label; relative packages are not permitted."
298 .to_string()
299 ))
300 );
301
302 assert_eq!(analyze("//foo")?.package_name, Some("foo"));
303
304 assert_eq!(
305 analyze("foo//bar"),
306 Err(LabelError(
307 "foo//bar must be a legal label; '//' cannot appear in the middle of the label."
308 .to_string()
309 ))
310 );
311 assert_eq!(
312 analyze("//foo//bar"),
313 Err(LabelError(
314 "//foo//bar must be a legal label; '//' cannot appear in the middle of the label."
315 .to_string()
316 ))
317 );
318 assert_eq!(
319 analyze("foo//bar:baz"),
320 Err(LabelError(
321 "foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
322 .to_string()
323 ))
324 );
325 assert_eq!(
326 analyze("//foo//bar:baz"),
327 Err(LabelError(
328 "//foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
329 .to_string()
330 ))
331 );
332
333 assert_eq!(
334 analyze("//azAZ09/-. $()_:baz")?.package_name,
335 Some("azAZ09/-. $()_")
336 );
337 assert_eq!(
338 analyze("//bar#:baz"),
339 Err(LabelError(
340 "//bar#:baz must be a legal label; package names may contain only A-Z, \
341 a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'."
342 .to_string()
343 ))
344 );
345 assert_eq!(
346 analyze("//bar/:baz"),
347 Err(LabelError(
348 "//bar/:baz must be a legal label; package names may not end with '/'.".to_string()
349 ))
350 );
351
352 assert_eq!(analyze("@repo//foo/bar")?.package_name, Some("foo/bar"));
353 assert_eq!(analyze("//foo/bar")?.package_name, Some("foo/bar"));
354 assert_eq!(
355 analyze("foo/bar"),
356 Err(LabelError(
357 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
358 ))
359 );
360
361 assert_eq!(analyze("@repo//foo")?.package_name, Some("foo"));
362 assert_eq!(analyze("//foo")?.package_name, Some("foo"));
363 assert_eq!(
364 analyze("foo"),
365 Err(LabelError(
366 "foo must be a legal label; relative packages are not permitted.".to_string()
367 ))
368 );
369
370 Ok(())
371 }
372
373 #[test]
374 fn test_name_parsing() -> Result<()> {
375 assert_eq!(analyze("//foo:baz")?.name, "baz");
376 assert_eq!(analyze("//foo:baz/qux")?.name, "baz/qux");
377
378 assert_eq!(
379 analyze("//bar:"),
380 Err(LabelError(
381 "//bar: must be a legal label; empty target name.".to_string()
382 ))
383 );
384 assert_eq!(analyze("//foo")?.name, "foo");
385
386 assert_eq!(
387 analyze("//bar:/foo"),
388 Err(LabelError(
389 "//bar:/foo must be a legal label; target names may not start with '/'."
390 .to_string()
391 ))
392 );
393
394 assert_eq!(analyze("@repo//foo/bar")?.name, "bar");
395 assert_eq!(analyze("//foo/bar")?.name, "bar");
396 assert_eq!(
397 analyze("foo/bar"),
398 Err(LabelError(
399 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
400 ))
401 );
402
403 assert_eq!(analyze("@repo//foo")?.name, "foo");
404 assert_eq!(analyze("//foo")?.name, "foo");
405 assert_eq!(
406 analyze("foo"),
407 Err(LabelError(
408 "foo must be a legal label; relative packages are not permitted.".to_string()
409 ))
410 );
411
412 Ok(())
413 }
414
415 #[test]
416 fn test_packages() -> Result<()> {
417 assert_eq!(analyze("@repo//:baz")?.packages(), Vec::<&str>::new());
418 assert_eq!(analyze("@repo//foo:baz")?.packages(), vec!["foo"]);
419 assert_eq!(
420 analyze("@repo//foo/bar:baz")?.packages(),
421 vec!["foo", "bar"]
422 );
423
424 Ok(())
425 }
426}