...

Text file src/github.com/bazelbuild/bazel-gazelle/internal/bzlmod/go_mod.bzl

Documentation: github.com/bazelbuild/bazel-gazelle/internal/bzlmod

     1# Copyright 2023 The Bazel Authors. All rights reserved.
     2#
     3# Licensed under the Apache License, Version 2.0 (the "License");
     4# you may not use this file except in compliance with the License.
     5# You may obtain a copy of the License at
     6#
     7#    http://www.apache.org/licenses/LICENSE-2.0
     8#
     9# Unless required by applicable law or agreed to in writing, software
    10# distributed under the License is distributed on an "AS IS" BASIS,
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12# See the License for the specific language governing permissions and
    13# limitations under the License.
    14
    15visibility([
    16    "//tests/bzlmod/...",
    17])
    18
    19def deps_from_go_mod(module_ctx, go_mod_label):
    20    """Loads the entries from a go.mod file.
    21
    22    Args:
    23        module_ctx: a https://bazel.build/rules/lib/module_ctx object passed
    24            from the MODULE.bazel call.
    25        go_mod_label: a Label for a `go.mod` file.
    26
    27    Returns:
    28        a tuple (Go module path, deps, replace map), where deps is a list of structs representing
    29        `require` statements from the go.mod file.
    30    """
    31    _check_go_mod_name(go_mod_label.name)
    32
    33    go_mod_path = module_ctx.path(go_mod_label)
    34    go_mod_content = module_ctx.read(go_mod_path)
    35    go_mod = parse_go_mod(go_mod_content, go_mod_path)
    36
    37    if go_mod.go[0] != 1 or go_mod.go[1] < 17:
    38        # go.mod files only include entries for all transitive dependencies as
    39        # of Go 1.17.
    40        fail("go_deps.from_file requires a go.mod file generated by Go 1.17 or later. Fix {} with 'go mod tidy -go=1.17'.".format(go_mod_label))
    41
    42    deps = []
    43    for require in go_mod.require:
    44        deps.append(struct(
    45            path = require.path,
    46            version = require.version,
    47            indirect = require.indirect,
    48        ))
    49
    50    return go_mod.module, deps, go_mod.replace_map
    51
    52def parse_go_mod(content, path):
    53    # See https://go.dev/ref/mod#go-mod-file.
    54
    55    # Valid directive values understood by this parser never contain tabs or
    56    # carriage returns, so we can simplify the parsing below by canonicalizing
    57    # whitespace upfront.
    58    content = content.replace("\t", " ").replace("\r", " ")
    59
    60    state = {
    61        "module": None,
    62        "go": None,
    63        "require": [],
    64        "replace": {},
    65    }
    66
    67    current_directive = None
    68    for line_no, line in enumerate(content.splitlines(), 1):
    69        tokens, comment = _tokenize_line(line, path, line_no)
    70        if not tokens:
    71            continue
    72
    73        if not current_directive:
    74            if tokens[0] not in ["module", "go", "require", "replace", "exclude", "retract", "toolchain"]:
    75                fail("{}:{}: unexpected token '{}' at start of line".format(path, line_no, tokens[0]))
    76            if len(tokens) == 1:
    77                fail("{}:{}: expected another token after '{}'".format(path, line_no, tokens[0]))
    78
    79            # The 'go' directive only has a single-line form and is thus parsed
    80            # here rather than in _parse_directive.
    81            if tokens[0] == "go":
    82                if len(tokens) == 1:
    83                    fail("{}:{}: expected another token after 'go'".format(path, line_no))
    84                if state["go"] != None:
    85                    fail("{}:{}: unexpected second 'go' directive".format(path, line_no))
    86                state["go"] = tokens[1]
    87                if len(tokens) > 2:
    88                    fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[2], tokens[1]))
    89
    90            if tokens[1] == "(":
    91                current_directive = tokens[0]
    92                if len(tokens) > 2:
    93                    fail("{}:{}: unexpected token '{}' after '('".format(path, line_no, tokens[2]))
    94                continue
    95
    96            _parse_directive(state, tokens[0], tokens[1:], comment, path, line_no)
    97
    98        elif tokens[0] == ")":
    99            current_directive = None
   100            if len(tokens) > 1:
   101                fail("{}:{}: unexpected token '{}' after ')'".format(path, line_no, tokens[1]))
   102            continue
   103
   104        else:
   105            _parse_directive(state, current_directive, tokens, comment, path, line_no)
   106
   107    module = state["module"]
   108    if not module:
   109        fail("Expected a module directive in go.mod file")
   110
   111    go = state["go"]
   112    if not go:
   113        # "As of the Go 1.17 release, if the go directive is missing, go 1.16 is assumed."
   114        go = "1.16"
   115
   116    # The go directive can contain patch and pre-release versions, but we omit them.
   117    major, minor = go.split(".")[:2]
   118
   119    return struct(
   120        module = module,
   121        go = (int(major), int(minor)),
   122        require = tuple(state["require"]),
   123        replace_map = state["replace"],
   124    )
   125
   126def _parse_directive(state, directive, tokens, comment, path, line_no):
   127    if directive == "module":
   128        if state["module"] != None:
   129            fail("{}:{}: unexpected second 'module' directive".format(path, line_no))
   130        if len(tokens) > 1:
   131            fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[1]))
   132        state["module"] = tokens[0]
   133    elif directive == "require":
   134        if len(tokens) != 2:
   135            fail("{}:{}: expected module path and version in 'require' directive".format(path, line_no))
   136        state["require"].append(struct(
   137            path = tokens[0],
   138            version = tokens[1],
   139            indirect = comment == "indirect",
   140        ))
   141    elif directive == "replace":
   142        # A replace directive might use a local file path beginning with ./ or ../
   143        # These are not supported with gazelle~go_deps.
   144        if (len(tokens) == 3 and tokens[2][0] == ".") or (len(tokens) > 3 and tokens[3][0] == "."):
   145            fail("{}:{}: local file path not supported in replace directive: '{}'".format(path, line_no, tokens[2]))
   146
   147        # replacements key off of the from_path
   148        from_path = tokens[0]
   149
   150        # pattern: replace from_path => to_path to_version
   151        if len(tokens) == 4 and tokens[1] == "=>":
   152            state["replace"][from_path] = struct(
   153                from_version = None,
   154                to_path = tokens[2],
   155                version = _canonicalize_raw_version(tokens[3]),
   156            )
   157        # pattern: replace from_path from_version => to_path to_version
   158        elif len(tokens) == 5 and tokens[2] == "=>":
   159            state["replace"][from_path] = struct(
   160                from_version = _canonicalize_raw_version(tokens[1]),
   161                to_path = tokens[3],
   162                version = _canonicalize_raw_version(tokens[4]),
   163            )
   164        else:
   165            fail(
   166                "{}:{}: replace directive must follow pattern: ".format(path, line_no) + 
   167                "'replace from_path from_version => to_path to_version' or " +
   168                "'replace from_path => to_path to_version'"
   169            )
   170
   171    # TODO: Handle exclude.
   172
   173def _tokenize_line(line, path, line_no):
   174    tokens = []
   175    r = line
   176    for _ in range(len(line)):
   177        r = r.strip()
   178        if not r:
   179            break
   180
   181        if r[0] == "`":
   182            end = r.find("`", 1)
   183            if end == -1:
   184                fail("{}:{}: unterminated raw string".format(path, line_no))
   185
   186            tokens.append(r[1:end])
   187            r = r[end + 1:]
   188
   189        elif r[0] == "\"":
   190            value = ""
   191            escaped = False
   192            found_end = False
   193            for pos in range(1, len(r)):
   194                c = r[pos]
   195
   196                if escaped:
   197                    value += c
   198                    escaped = False
   199                    continue
   200
   201                if c == "\\":
   202                    escaped = True
   203                    continue
   204
   205                if c == "\"":
   206                    found_end = True
   207                    break
   208
   209                value += c
   210
   211            if not found_end:
   212                fail("{}:{}: unterminated interpreted string".format(path, line_no))
   213
   214            tokens.append(value)
   215            r = r[pos + 1:]
   216
   217        elif r.startswith("//"):
   218            # A comment always ends the current line
   219            return tokens, r[len("//"):].strip()
   220
   221        else:
   222            token, _, r = r.partition(" ")
   223            tokens.append(token)
   224
   225    return tokens, None
   226
   227def sums_from_go_mod(module_ctx, go_mod_label):
   228    """Loads the entries from a go.sum file given a go.mod Label.
   229
   230    Args:
   231        module_ctx: a https://bazel.build/rules/lib/module_ctx object
   232            passed from the MODULE.bazel call.
   233        go_mod_label: a Label for a `go.mod` file. This label is used
   234            to find the associated `go.sum` file.
   235
   236    Returns:
   237        A Dict[(string, string) -> (string)] is retruned where each entry
   238        is defined by a Go Module's sum:
   239            (path, version) -> (sum)
   240    """
   241    _check_go_mod_name(go_mod_label.name)
   242
   243    # We go through a Label so that the module extension is restarted if go.sum
   244    # changes. We have to use a canonical label as we may not have visibility
   245    # into the module that provides the go.sum.
   246    go_sum_label = Label("@@{}//{}:{}".format(
   247        go_mod_label.workspace_name,
   248        go_mod_label.package,
   249        "go.sum",
   250    ))
   251    go_sum_content = module_ctx.read(go_sum_label)
   252    return parse_go_sum(go_sum_content)
   253
   254def parse_go_sum(content):
   255    hashes = {}
   256    for line in content.splitlines():
   257        path, version, sum = line.split(" ")
   258        version = _canonicalize_raw_version(version)
   259        if not version.endswith("/go.mod"):
   260            hashes[(path, version)] = sum
   261    return hashes
   262
   263def _check_go_mod_name(name):
   264    if name != "go.mod":
   265        fail("go_deps.from_file requires a 'go.mod' file, not '{}'".format(name))
   266
   267def _canonicalize_raw_version(raw_version):
   268    if raw_version.startswith("v"):
   269        return raw_version[1:]
   270    return raw_version

View as plain text