Skip to content

Commit

Permalink
Location expansion using pure functions
Browse files Browse the repository at this point in the history
Addressing review comment
#132 (comment)

Implement parsing of location expansion commands and label resolution as
pure functions that do not depend on the repository context. This allows
to test these functions in unit tests.
  • Loading branch information
aherrmann committed Nov 11, 2020
1 parent b04fe7c commit 5a4dfea
Showing 1 changed file with 106 additions and 47 deletions.
153 changes: 106 additions & 47 deletions nixpkgs/private/location_expansion.bzl
Original file line number Diff line number Diff line change
@@ -1,69 +1,128 @@
load("@bazel_skylib//lib:paths.bzl", "paths")

def expand_location(repository_ctx, string, labels, attr = None):
"""Expand `$(location label)` to a path.
def parse_expand_location(string):
"""Parse a string that might contain location expansion commands.
Raises an error on unexpected occurrences of `$`.
Use `$$` to insert a verbatim `$`.
Generates a list of pairs of command and argument.
The command can have the following values:
- `string`: argument is a string, append it to the result.
- `location`: argument is a label, append its location to the result.
Attrs:
repository_ctx: The repository rule context.
string: string, Replace instances of `$(location )` in this string.
labels: dict from label to path: Known label to path mappings.
attr: string, The rule attribute to use for error reporting.
string: string, The string to parse.
Returns:
The string with all instances of `$(location )` replaced by paths.
(result, error):
result: The generated list of pairs of command and argument.
error: string or None, This is set if an error occurred.
"""
result = ""
result = []
offset = 0
len_string = len(string)

# Step through occurrences of `$`. This is bounded by the length of the string.
for _ in range(len(string)):
start = string.find("$", offset)
if start == -1:
result += string[offset:]
for _ in range(len_string):
# Find the position of the next `$`.
position = string.find("$", offset)
if position == -1:
position = len_string

# Append the in-between literal string.
if offset < position:
result.append(("string", string[offset:position]))

# Terminate at the end of the string.
if position == len_string:
break
else:
result += string[offset:start]
if start + 1 == len(string):
fail("Unescaped '$' in location expansion at end of input", attr)
elif string[start + 1] == "$":

# Parse the `$` command.
if string[position:].startswith("$$"):
# Insert verbatim '$'.
result += "$"
offset = start + 2
elif string[start + 1] == "(":
group_start = start + 2
result.append(("string", "$"))
offset = position + 2
elif string[position:].startswith("$("):
# Expand a location command.
group_start = position + 2
group_end = string.find(")", group_start)
if group_end == -1:
fail("Unbalanced parentheses in location expansion for '{}'.".format(string[start:]), attr)
return (None, "Unbalanced parentheses in location expansion for '{}'.".format(string[position:]))

group = string[group_start:group_end]
command = None
if group.startswith("location "):
label_str = group[len("location "):]
label_candidates = [
(lbl, path)
for (lbl, path) in labels.items()
if lbl.relative(label_str) == lbl
]
if len(label_candidates) == 0:
fail("Unknown label '{}' in location expansion for '{}'.".format(label_str, string), attr)
elif len(label_candidates) > 1:
fail(
"Ambiguous label '{}' in location expansion for '{}'. Candidates: {}".format(
label_str,
string,
", ".join([str(lbl) for lbl in label_candidates]),
),
attr,
)
location = paths.join(".", paths.relativize(
str(repository_ctx.path(label_candidates[0][1])),
str(repository_ctx.path(".")),
))
result += location
command = ("location", label_str)
else:
fail("Unrecognized location expansion '$({})'.".format(group), attr)
return (None, "Unrecognized location expansion '$({})'.".format(group))

result.append(command)
offset = group_end + 1
else:
fail("Unescaped '$' in location expansion at position {} of input.".format(start), attr)
return (None, "Unescaped '$' in location expansion at position {} of input.".format(position))

return (result, None)

def resolve_label(label_str, labels):
"""Find the label that corresponds to the given string.
Attr:
label_str: string, String representation of a label.
labels: dict from Label to path: Known label to path mappings.
Returns:
(path, error):
path: path, The path to the resolved label
error: string or None, This is set if an error occurred.
"""
label_candidates = [
(lbl, path)
for (lbl, path) in labels.items()
if lbl.relative(label_str) == lbl
]

if len(label_candidates) == 0:
return (None, "Unknown label '{}' in location expansion.".format(label_str))
elif len(label_candidates) > 1:
return (None, "Ambiguous label '{}' in location expansion. Candidates: {}".format(
label_str,
", ".join([str(lbl) for (lbl, _) in label_candidates]),
))

return (label_candidates[0][1], None)

def expand_location(repository_ctx, string, labels, attr = None):
"""Expand `$(location label)` to a path.
Raises an error on unexpected occurrences of `$`.
Use `$$` to insert a verbatim `$`.
Attrs:
repository_ctx: The repository rule context.
string: string, Replace instances of `$(location )` in this string.
labels: dict from label to path: Known label to path mappings.
attr: string, The rule attribute to use for error reporting.
Returns:
The string with all instances of `$(location )` replaced by paths.
"""
(parsed, error) = parse_expand_location(string)
if error != None:
fail(error, attr)

result = ""
for (command, argument) in parsed:
if command == "string":
result += argument
elif command == "location":
(label, error) = resolve_label(argument, labels)
if error != None:
fail(error, attr)

result += paths.join(".", paths.relativize(
str(repository_ctx.path(label)),
str(repository_ctx.path(".")),
))
else:
fail("Internal error: Unknown location expansion command '{}'.".format(command), attr)

return result

0 comments on commit 5a4dfea

Please sign in to comment.