uv/spec/dependabot/uv/file_parser_spec.rb
# typed: false
# frozen_string_literal: true

require "spec_helper"
require "dependabot/dependency_file"
require "dependabot/source"
require "dependabot/uv/file_parser"
require_common_spec "file_parsers/shared_examples_for_file_parsers"

RSpec.describe Dependabot::Uv::FileParser do
  let(:requirements_fixture_name) { "version_specified.txt" }
  let(:requirements_body) { fixture("requirements", requirements_fixture_name) }
  let(:requirements) do
    Dependabot::DependencyFile.new(
      name: "requirements.txt",
      content: requirements_body
    )
  end
  let(:files) { [requirements] }
  let(:reject_external_code) { false }
  let(:source) do
    Dependabot::Source.new(
      provider: "github",
      repo: "gocardless/bump",
      directory: "/"
    )
  end
  let(:parser) do
    described_class.new(
      dependency_files: files,
      source: source,
      reject_external_code: reject_external_code
    )
  end

  it_behaves_like "a dependency file parser"

  describe "parse" do
    subject(:dependencies) { parser.parse }

    its(:length) { is_expected.to eq(5) }

    context "with a version specified" do
      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with a .python-version file" do
      let(:files) { [requirements, python_version_file] }
      let(:python_version_file) do
        Dependabot::DependencyFile.new(
          name: ".python-version",
          content: "2.7.18\n"
        )
      end

      its(:length) { is_expected.to eq(5) }
    end

    context "with jinja templates" do
      let(:requirements_fixture_name) { "jinja_requirements.txt" }

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with comments" do
      let(:requirements_fixture_name) { "comments.txt" }

      its(:length) { is_expected.to eq(2) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with markers" do
      context "when the marker <= 2.6" do
        before do
          allow(parser).to receive(:python_raw_version).and_return("2.6")
        end

        let(:requirements_fixture_name) { "markers.txt" }

        it "then the dependency version should be 1.0.4" do
          expect(dependencies.length).to eq(1)

          dependency = dependencies.first

          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("distro")
          expect(dependency.version).to eq("1.0.4")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==1.0.4",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end

      context "when the marker => 2.7" do
        before do
          allow(parser).to receive(:python_raw_version).and_return("2.7")
        end

        let(:requirements_fixture_name) { "markers.txt" }

        it "then the dependency version should be 1.3.0" do
          expect(dependencies.length).to eq(1)

          dependency = dependencies.first

          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("distro")
          expect(dependency.version).to eq("1.3.0")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==1.3.0",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end

      context "when there is a combination of multiple conditions with 'and' in a marker" do
        before do
          allow(parser).to receive(:python_raw_version).and_return("3.13.1")
        end

        # python_version >= '3.0' and python_version <= '3.7'
        let(:requirements_fixture_name) { "markers_with_combination_of_conditions.txt" }

        it "then the dependency version should be 1.3.0" do
          expect(dependencies.length).to eq(1)

          dependency = dependencies.first

          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("arrow")
          expect(dependency.version).to eq("1.3.0")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==1.3.0",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end

      context "when including a < in the requirement" do
        let(:requirements_fixture_name) { "markers_2.txt" }

        it "parses only the >= marker" do
          expect(dependencies.length).to eq(1)

          dependency = dependencies.first

          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("cryptography")
          expect(dependency.version).to eq("2.7")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.7",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end

      context "when the marker is malformed" do
        before do
          allow(parser).to receive(:python_raw_version).and_return("3.13.3")
        end

        let(:requirements_fixture_name) { "malformed_markers.txt" }

        it "does not return any dependencies" do
          expect(dependencies).to be_empty
        end
      end
    end

    context "with extras" do
      let(:requirements_fixture_name) { "extras.txt" }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2[bar,foo]")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with an 'unsafe' name" do
      let(:requirements_body) { "mypy_extensions==0.2.0" }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "normalises the name" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("mypy-extensions")
        end
      end
    end

    context "with invalid lines" do
      let(:requirements_fixture_name) { "invalid_lines.txt" }

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with tarball path dependencies" do
      let(:files) { [pyproject, requirements, tarball_path_dependency] }
      let(:requirements) do
        Dependabot::DependencyFile.new(
          name: "requirements.txt",
          content: fixture("requirements", "tarball_path_dependency")
        )
      end
      let(:pyproject) do
        Dependabot::DependencyFile.new(
          name: "pyproject.toml",
          content: fixture("pyproject_files", "tarball_path_dependency.toml")
        )
      end
      let(:tarball_path_dependency) do
        Dependabot::DependencyFile.new(
          name: "taxtea-0.6.0.tar.gz",
          content: fixture("path_dependencies", "taxtea-0.6.0.tar.gz")
        )
      end

      describe "the tarball dependency requirement" do
        it "is not parsed" do
          expect(dependencies).to eq([])
        end
      end
    end

    context "when itself is required" do
      let(:files) { [requirements] }
      let(:requirements_fixture_name) { "cascading.txt" }
      let(:requirements) do
        Dependabot::DependencyFile.new(
          name: "more_requirements.txt",
          content: requirements_body
        )
      end

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with an invalid value" do
      let(:requirements_fixture_name) { "invalid_value.txt" }

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with invalid options" do
      let(:requirements_fixture_name) { "invalid_options.txt" }

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with invalid requirements" do
      let(:requirements_fixture_name) { "invalid_requirements.txt" }

      it "raises a Dependabot::DependencyFileNotEvaluatable error" do
        expect { parser.parse }
          .to raise_error(Dependabot::DependencyFileNotEvaluatable)
      end
    end

    context "with remote constraints" do
      let(:requirements_fixture_name) { "remote_constraints.txt" }

      its(:length) { is_expected.to eq(0) }
    end

    context "with no version specified" do
      let(:requirements_fixture_name) { "version_not_specified.txt" }

      its(:length) { is_expected.to eq(2) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to be_nil
          expect(dependency.requirements.first[:requirement]).to be_nil
        end
      end
    end

    context "with prefix matching specified" do
      let(:requirements_fixture_name) { "prefix_match.txt" }

      its(:length) { is_expected.to eq(2) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to be_nil
          expect(dependency.requirements.first[:requirement]).to eq("==2.6.*")
        end
      end
    end

    context "with a version specified as between two constraints" do
      let(:requirements_fixture_name) { "version_between_bounds.txt" }

      its(:length) { is_expected.to eq(2) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to be_nil
          expect(dependency.requirements.first[:requirement])
            .to eq("<=3.0.0,==2.6.1")
        end
      end
    end

    context "with a git dependency" do
      let(:requirements_fixture_name) { "with_git_dependency.txt" }

      its(:length) { is_expected.to eq(2) }
    end

    context "with a file dependency" do
      let(:requirements_fixture_name) { "with_path_dependency.txt" }

      its(:length) { is_expected.to eq(1) }
    end

    context "with requirements-dev.txt" do
      let(:file) { [requirements] }
      let(:requirements) do
        Dependabot::DependencyFile.new(
          name: "requirements-dev.txt",
          content: fixture("requirements", "version_specified.txt")
        )
      end

      its(:length) { is_expected.to eq(5) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "requirements-dev.txt",
              groups: ["dev-dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with dev-requirements.txt" do
      let(:file) { [requirements] }
      let(:requirements) do
        Dependabot::DependencyFile.new(
          name: "dev-requirements.txt",
          content: fixture("requirements", "version_specified.txt")
        )
      end

      its(:length) { is_expected.to eq(5) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "dev-requirements.txt",
              groups: ["dev-dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with requirements/dev.txt" do
      let(:file) { [requirements] }
      let(:requirements) do
        Dependabot::DependencyFile.new(
          name: "requirements/dev.txt",
          content: fixture("requirements", "version_specified.txt")
        )
      end

      its(:length) { is_expected.to eq(5) }

      describe "the first dependency" do
        subject(:dependency) { dependencies.first }

        it "has the right details" do
          expect(dependency).to be_a(Dependabot::Dependency)
          expect(dependency.name).to eq("psycopg2")
          expect(dependency.version).to eq("2.6.1")
          expect(dependency.requirements).to eq(
            [{
              requirement: "==2.6.1",
              file: "requirements/dev.txt",
              groups: ["dev-dependencies"],
              source: nil
            }]
          )
        end
      end
    end

    context "with child requirement files" do
      let(:files) { [requirements, child_requirements] }
      let(:requirements_fixture_name) { "cascading.txt" }
      let(:child_requirements) do
        Dependabot::DependencyFile.new(
          name: "more_requirements.txt",
          content: fixture("requirements", "version_specified.txt")
        )
      end

      its(:length) { is_expected.to eq(6) }

      it "has the right details" do
        expect(dependencies).to contain_exactly(
          Dependabot::Dependency.new(
            name: "requests",
            version: "2.4.1",
            requirements: [{
              requirement: "==2.4.1",
              file: "requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          ),
          Dependabot::Dependency.new(
            name: "attrs",
            version: "18.0.0",
            requirements: [{
              requirement: "==18.0.0",
              file: "more_requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          ),
          Dependabot::Dependency.new(
            name: "aiocache[redis]",
            version: "0.10.0",
            requirements: [{
              requirement: "==0.10.0",
              file: "more_requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          ),
          Dependabot::Dependency.new(
            name: "luigi",
            version: "2.2.0",
            requirements: [{
              requirement: "==2.2.0",
              file: "more_requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          ),
          Dependabot::Dependency.new(
            name: "psycopg2",
            version: "2.6.1",
            requirements: [{
              requirement: "==2.6.1",
              file: "more_requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          ),
          Dependabot::Dependency.new(
            name: "pytest",
            version: "3.4.0",
            requirements: [{
              requirement: "==3.4.0",
              file: "more_requirements.txt",
              groups: ["dependencies"],
              source: nil
            }],
            package_manager: "uv"
          )
        )
      end
    end

    context "with a pip-compile file" do
      let(:files) { [manifest_file, generated_file] }
      let(:manifest_file) do
        Dependabot::DependencyFile.new(
          name: "requirements/test.in",
          content: fixture("pip_compile_files", manifest_fixture_name)
        )
      end
      let(:generated_file) do
        Dependabot::DependencyFile.new(
          name: "requirements/test.txt",
          content: fixture("requirements", generated_fixture_name)
        )
      end
      let(:manifest_fixture_name) { "unpinned.in" }
      let(:generated_fixture_name) { "pip_compile_unpinned.txt" }

      its(:length) { is_expected.to eq(13) }

      describe "top level dependencies" do
        subject(:dependencies) { parser.parse.select(&:top_level?) }

        its(:length) { is_expected.to eq(5) }

        describe "the first dependency" do
          subject(:dependency) { dependencies.first }

          it "has the right details" do
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("attrs")
            expect(dependency.version).to eq("17.3.0")
            expect(dependency.requirements).to eq(
              [{
                requirement: nil,
                file: "requirements/test.in",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end

        context "with filenames that can't be figured out" do
          let(:generated_file) do
            Dependabot::DependencyFile.new(
              name: "requirements.txt",
              content: fixture("requirements", generated_fixture_name)
            )
          end

          describe "the first dependency" do
            subject(:dependency) { dependencies.first }

            it "has the right details" do
              expect(dependency).to be_a(Dependabot::Dependency)
              expect(dependency.name).to eq("apipkg")
              expect(dependency.version).to eq("1.4")
              expect(dependency.requirements).to eq(
                [{
                  requirement: "==1.4",
                  file: "requirements.txt",
                  groups: ["dependencies"],
                  source: nil
                }]
              )
            end
          end

          describe "the second dependency" do
            subject(:dependency) { dependencies[1] }

            it "has the right details" do
              expect(dependency).to be_a(Dependabot::Dependency)
              expect(dependency.name).to eq("attrs")
              expect(dependency.version).to eq("17.3.0")
              expect(dependency.requirements).to contain_exactly(
                {
                  requirement: nil,
                  file: "requirements/test.in",
                  groups: ["dependencies"],
                  source: nil
                },
                {
                  requirement: "==17.3.0",
                  file: "requirements.txt",
                  groups: ["dependencies"],
                  source: nil
                }
              )
            end
          end
        end

        context "with a version bound" do
          let(:manifest_fixture_name) { "bounded.in" }
          let(:generated_fixture_name) { "pip_compile_bounded.txt" }

          describe "the first dependency" do
            subject(:dependency) { dependencies.first }

            it "has the right details" do
              expect(dependency).to be_a(Dependabot::Dependency)
              expect(dependency.name).to eq("attrs")
              expect(dependency.version).to eq("17.3.0")
              expect(dependency.requirements).to eq(
                [{
                  requirement: "<=17.4.0",
                  file: "requirements/test.in",
                  groups: ["dependencies"],
                  source: nil
                }]
              )
            end
          end

          it "returns the correct ecosystem and package manager set" do
            ecosystem = parser.ecosystem
            expect(ecosystem.name).to eq("uv")
            expect(ecosystem.package_manager.name).to eq("uv")
            file_path = File.expand_path("../../../helpers/requirements.txt", __dir__)
            uv_version = File.foreach(file_path).find { |line| line =~ /^uv==/ }.split("==").last.strip
            expect(ecosystem.package_manager.version.to_s).to eq(uv_version)
            expect(ecosystem.language.name).to eq("python")
          end
        end
      end

      context "with a mismatching name" do
        let(:generated_file) do
          Dependabot::DependencyFile.new(
            name: "requirements/test-funky.txt",
            content: fixture("requirements", generated_fixture_name)
          )
        end
        let(:generated_fixture_name) { "pip_compile_unpinned_renamed.txt" }

        describe "top level dependencies" do
          subject(:dependencies) { parser.parse.select(&:top_level?) }

          its(:length) { is_expected.to eq(5) }
        end
      end
    end

    context "with a pyproject file only" do
      let(:files) { [pyproject] }
      let(:pyproject) do
        Dependabot::DependencyFile.new(
          name: "pyproject.toml",
          content: fixture("pyproject_files", "pyproject_1_0_0.toml")
        )
      end

      # pydantic (from project.dependencies)
      # setuptools (from build-system.requires)
      # setuptools-scm (from build-system.requires)
      its(:length) { is_expected.to eq(3) }
    end

    context "with a pyproject.toml file with no dependencies" do
      let(:files) { [pyproject] }
      let(:pyproject) do
        Dependabot::DependencyFile.new(
          name: "pyproject.toml",
          content: fixture("pyproject_files", "pyproject_1_0_0_nodeps.toml")
        )
      end

      # setuptools
      # setuptools-scm
      its(:length) { is_expected.to eq(2) }
    end

    context "with a pyproject.toml in poetry format and a lock file" do
      let(:files) { [pyproject, poetry_lock] }
      let(:pyproject) do
        Dependabot::DependencyFile.new(
          name: "pyproject.toml",
          content: fixture("pyproject_files", "basic_poetry_dependencies.toml")
        )
      end
      let(:poetry_lock) do
        Dependabot::DependencyFile.new(
          name: "poetry.lock",
          content: fixture("poetry_locks", "poetry.lock")
        )
      end

      its(:length) { is_expected.to eq(36) }

      describe "top level dependencies" do
        subject(:dependencies) { parser.parse.select(&:top_level?) }

        its(:length) { is_expected.to eq(15) }

        describe "the first dependency" do
          subject(:dependency) { dependencies.first }

          it "has the right details" do
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("geopy")
            expect(dependency.version).to eq("1.14.0")
            expect(dependency.requirements).to eq(
              [{
                requirement: "^1.13",
                file: "pyproject.toml",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end
      end

      context "when dealing with a requirements.txt" do
        let(:files) { [pyproject, requirements] }
        let(:pyproject) do
          Dependabot::DependencyFile.new(
            name: "pyproject.toml",
            content: fixture("pyproject_files", "version_not_specified.toml")
          )
        end

        its(:length) { is_expected.to eq(6) }

        describe "the first dependency" do
          subject(:dependency) { dependencies.first }

          it "has the right details" do
            # requests is only in version_not_specified.toml
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("requests")
            expect(dependency.version).to be_nil
            expect(dependency.requirements).to eq(
              [{
                requirement: "*",
                file: "pyproject.toml",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end

        describe "the second dependency" do
          subject(:dependency) { dependencies[1] }

          it "has the right details" do
            # pytest is in both files, it picks version to be the lowest
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("pytest")
            expect(dependency.version).to eq("3.4.0")
            expect(dependency.requirements).to eq(
              [{
                requirement: "*",
                file: "pyproject.toml",
                groups: ["dependencies"],
                source: nil
              }, {
                requirement: "==3.4.0",
                file: "requirements.txt",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end

        describe "the third dependency" do
          subject(:dependency) { dependencies[2] }

          it "has the right details" do
            # psycopg2 only exists in version_specified.txt
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("psycopg2")
            expect(dependency.version).to eq("2.6.1")
            expect(dependency.requirements).to eq(
              [{
                requirement: "==2.6.1",
                file: "requirements.txt",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end

        describe "a dependency in requirements.txt that only exists in the lockfile" do
          subject(:dependency) { dependencies.find { |d| d.name == "attrs" } }

          it "has the right details" do
            # attrs2 exists in version_not_specified.lock and version_specified.txt
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("attrs")
            expect(dependency.version).to eq("18.0.0")
            # it only has 1 requirement since it isn't in version_not_specified.toml
            expect(dependency.requirements).to eq(
              [{
                requirement: "==18.0.0",
                file: "requirements.txt",
                groups: ["dependencies"],
                source: nil
              }]
            )
          end
        end
      end
    end

    context "with a uv.lock file" do
      let(:files) { [pyproject, uv_lock] }
      let(:pyproject) do
        Dependabot::DependencyFile.new(
          name: "pyproject.toml",
          content: fixture("pyproject_files", "uv_simple.toml")
        )
      end
      let(:uv_lock) do
        Dependabot::DependencyFile.new(
          name: "uv.lock",
          content: fixture("uv_locks", "simple.lock")
        )
      end

      its(:length) { is_expected.to eq(8) }

      describe "top level dependencies" do
        subject(:dependencies) { parser.parse.select(&:top_level?) }

        its(:length) { is_expected.to eq(3) }

        describe "the first dependency" do
          subject(:dependency) { dependencies.first }

          it "has the right details" do
            expect(dependency).to be_a(Dependabot::Dependency)
            expect(dependency.name).to eq("requests")
            expect(dependency.version).to eq("2.32.3")
            expect(dependency.requirements).to eq(
              [{
                requirement: ">=2.31.0",
                file: "pyproject.toml",
                groups: [],
                source: nil
              }]
            )
          end
        end
      end
    end
  end
end
