├── .codeclimate.yml
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── premerge.yaml
│ └── release.yaml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yml
├── .mdlrc
├── .yamllint.yaml
├── LICENSE
├── README.md
├── ci
├── lint.sh
├── release.sh
└── test.sh
├── docs
└── release-notes
│ ├── next.md
│ ├── template.md
│ ├── v0.1.0.md
│ ├── v0.2.0.md
│ ├── v0.2.1.md
│ ├── v0.3.0.md
│ ├── v0.3.1.md
│ ├── v0.4.0.md
│ ├── v0.5.0.md
│ ├── v0.6.0.md
│ ├── v0.6.1.md
│ ├── v0.6.2.md
│ ├── v0.7.0.md
│ └── v0.7.1.md
├── go.mod
├── go.sum
├── images
├── dependency-chains.dot
├── dependency-chains.jpg
├── full.dot
├── full.jpg
├── shared-dependencies.dot
└── shared-dependencies.jpg
├── internal
├── analysis
│ ├── analysis.go
│ ├── analysis_test.go
│ └── testdata
│ │ ├── DeprecatedUpdate.yaml
│ │ ├── MissingTimestamp.yaml
│ │ ├── NoUpdates.yaml
│ │ ├── OneDep.yaml
│ │ ├── OneDirectOneIndirect.yaml
│ │ └── TwoDeps.yaml
├── depgraph
│ ├── colours.go
│ ├── deps_mod.go
│ ├── deps_pkg.go
│ ├── graph.go
│ ├── module.go
│ ├── package.go
│ ├── parser.go
│ ├── query.go
│ └── query_test.go
├── graph
│ ├── graph.go
│ ├── graph_test.go
│ ├── node.go
│ └── node_test.go
├── logger
│ ├── encoder.go
│ └── logger.go
├── modules
│ ├── modules.go
│ ├── modules_test.go
│ ├── packages.go
│ └── testdata
│ │ ├── GoListError.yaml
│ │ ├── InvalidListOutput.yaml
│ │ ├── LoadError.yaml
│ │ ├── LoadErrorNotFatal.yaml
│ │ ├── NoDependencies.yaml
│ │ ├── NoModule.yaml
│ │ └── ReplacedDependency.yaml
├── parsers
│ ├── style.go
│ └── style_test.go
├── printer
│ ├── clustering.go
│ └── printer.go
├── query
│ ├── grammar.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── tokenizer.go
│ ├── tokenizer_test.go
│ └── tokens.go
├── reveal
│ ├── replacements.go
│ ├── replacements_test.go
│ └── testdata
│ │ ├── mainModule
│ │ └── go.mod
│ │ ├── moduleA
│ │ └── go.mod
│ │ └── moduleB
│ │ └── go.mod
├── testutil
│ ├── log.go
│ └── test_module.go
└── util
│ ├── exec.go
│ └── fileutil.go
├── main.go
├── main_strings.go
├── main_test.go
├── mdl_style.rb
├── tools.mod
└── tools.sum
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | version: "2"
4 | checks:
5 | method-complexity:
6 | config:
7 | threshold: 15
8 | return-statements:
9 | config:
10 | threshold: 5
11 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | This project welcomes contributions from the community. In order to help you make the best use of
4 | your time while contributing you will find some guidelines below that should clarify any doubts you
5 | might have and answers most of the usual questions that arise as part of general open-source
6 | development.
7 |
8 | ## Table of contents
9 |
10 | - [How to raise an Issue](#how-to-raise-an-issue)
11 | - [Opening a new Issue](#opening-a-new-issue)
12 | - [Do's and Don'ts](#dos-and-donts)
13 | - [Addressing an Issue](#addressing-an-issue)
14 | - [How to create a Pull Request](#how-to-create-a-pull-request)
15 | - [Opening a new Pull Request](#opening-a-new-pull-request)
16 | - [Writing up a good description](#writing-up-a-good-description)
17 | - [Summary](#summary)
18 | - [Tests](#tests)
19 | - [Code guidelines](#code-guidelines)
20 | - [Style and linters](#style-and-linters)
21 | - [Continuous integration](#continuous-integration)
22 |
23 | ## How to raise an Issue
24 |
25 | ### Opening a new Issue
26 |
27 | Whether you think you have found a bug, encountered some unexpected behaviour, a problem that this
28 | project could potentially solve or a suggestion for an improvement do not hesitate to open an
29 | [Issue](https://github.com/Helcaraxan/gomod/issues) describing it. Do keep in mind the following
30 | reminders to guarantee yourself the highest chance of getting a quick answer to your question and
31 | any related code changes.
32 |
33 | ### Do's and Don'ts
34 |
35 | Do:
36 |
37 | - Follow the checklist provided by the template when creating a new Issue.
38 | - Be polite and constructive in your questions and any follow-up discussions.
39 | - Focus on the _What_ and the _Why_ first, instead of the _How_. This generally allows for more
40 | open-minded thinking and a wider range of solutions.
41 |
42 | Don't:
43 |
44 | - Make demands. Open-source projects are generally maintained on people's free time and they will be
45 | very unlikely to help you if they feel you do not value that aspect of their work.
46 | - Tell someone in a discussion that they are wrong and you are right. Instead provide the arguments
47 | that explain why your approach has more advantages and / or less disadvantages than theirs.
48 |
49 | ### Addressing an Issue
50 |
51 | An Issue has been discussed and a way forward has been found? Now it's time to actually implement
52 | what has been agreed upon. Whether you are the person who raised the Issue first, a participant in
53 | the discussion or simply someone who wants to contribute to the project your next step will be to
54 | [open a Pull Request](#how-to-create-a-pull-request) with the necessary changes.
55 |
56 | If the changes are complex or might require multiple consequetive Pull Requests it is best to update
57 | the corresponding Issue and tell other participants that you will be taking on the work. Bonus
58 | points if you can also provide a rough estimate for when you think you will be able to deliver the
59 | work. This will help others understand what to expect and prevents two developers of working on the
60 | same thing.
61 |
62 | ## How to create a Pull Request
63 |
64 | ### Opening a new Pull Request
65 |
66 | If you have an idea for a change to the codebase take a pause before starting to write the actual
67 | change you have in mind. It is generally useful to answer a few basic questions first. This will
68 | help you decide whether to open up a PR with the suggested changes or if it might be more
69 | appropriate to [raise an issue](#how-to-raise-an-issue) instead to discuss the changes first.
70 |
71 | - What kind of change do you have in mind?
72 | - A simple bugfix.
73 |
74 | _Go and open that PR 😄
75 |
76 | - A complex bugfix.
77 |
78 | _Unless there is already a related open Issue where an agreement has been found on how to fix
79 | the bug it is best to first open an Issue (if it doesn't exist yet) or describe the fix you are
80 | suggesting._
81 |
82 | - An improvement of an existing feature.
83 |
84 | _Does the improvement serve a niche use-case of the feature? Will the change break or modify
85 | existing use-cases of the feature? Does the improvement require significant changes to the code?
86 | Is the improved behaviour hard to test?_
87 |
88 | _If you can answer yes to any of the above questions it is best to open an Issue first to
89 | discuss your idea._
90 |
91 | - A new feature.
92 |
93 | _Open an Issue first to discuss your idea and check that it is compatible with other planned
94 | features and has the blessing of the project maintainer._
95 |
96 | ### Writing up a good description
97 |
98 | Below you will find some examples on how to properly fill in the description of your Pull Request. A
99 | well-written description is often a pleasure to read and generally invites a project maintainer to
100 | provide quick and high-quality feedback. On the other hand, a poorly written or even absent PR
101 | description is less likely to get the attention of a project maintainer and **might even lead to
102 | your PR being closed without review** until its description is up to standards.
103 |
104 | #### Summary
105 |
106 | An examples of a good summary is:
107 | > Larger Go modules tend to generate huge dependency graph images that have large holes in them
108 | > without any nodes.
109 | >
110 | > Following the discussions in issue #issue-ref, this PR adds support for the clustering of nodes
111 | > that share the same predecessors in the dependency graph which leads to significantly smaller
112 | > image files containing less holes. The clustering is implemented via a new `depCluster` type
113 | > that exposes a few useful methods as well as some utility functions that compute the clusters for
114 | > a given dependency graph.
115 | >
116 | > Support for printing dependency graphs while using the new `depCluster` type will be added in a
117 | > follow-up PR.
118 |
119 | This description briefly describes the nature of the change and why it is beneficial. It also
120 | appropriately references the issue where the change was discussed up front before being implemented
121 | and acknowledges that there will be more changes required which will be part of a future PR.
122 | Alternatively you could also write something similar to:
123 | > Ensuring that the documentation of the project is appropriately maintained as the set of
124 | > available features changes and grows over time is tedious and error-prone if done manually. It is
125 | > much better to automate all the maintenance actions we can. Besides facilitating the regeneration
126 | > of the documentation when changed such automation also allows to test for any forgotten changes
127 | > by running the generation script as part of continuous integration.
128 | >
129 | > This PR creates a script that automatically generates some parts of the documentation and updates
130 | > the continuous integration test script to check that the documentation is being kept up-to-date.
131 |
132 | Here the description makes a clear case for the "why" of the suggested change. The change is small
133 | enough that there's not necessarily a need to raise an issue up front and any details can be
134 | discussed as part of the PR's review process.
135 |
136 | An examples of a bad summary would be:
137 | > Fix for #issue-ref
138 |
139 | This does not provide any context on the nature of the fix that is being implemented, nor why the
140 | suggested change is the _best_ fix for this specific issue. Another bad example would be to simply
141 | have no summary with the placeholder still left behind:
142 | > _Explain what the goal of your PR is, why it is needed and how it achieves the described goal._
143 |
144 | #### Tests
145 |
146 | Again a good example first:
147 | > This PR is a bugfix and does not change the functionality of the code it touches. It does add a
148 | > few new testcases in the existing unit-tests to cover the edge-cases that exposed the bug in the
149 | > first place.
150 |
151 | Or otherwise:
152 | > New tests have been added for each of the methods exposed by the new `OptionParser` type. The
153 | > tests use a standard iteration over a list of testcases which together cover all expected usages
154 | > of the new type.
155 |
156 | Do not write something like:
157 | > The feature set added by this PR is too wide for unit-tests. We might want to add integration
158 | > tests at a later stage.
159 |
160 | Although this explanation does acknowledge the lack of tests it does not provide an adequate reason
161 | for not implementing the integration test framework first before implementing the proposed change.
162 |
163 | ## Code guidelines
164 |
165 | ### Style and linters
166 |
167 | This project encourages the use of the general Go styleguide as described in
168 | [Effective Go](https://golang.org/doc/effective_go.html) and on the
169 | [Go wiki](https://github.com/golang/go/wiki/CodeReviewComments).
170 |
171 | The more fine-grained codestyle points that are not covered by the general guidelines are enforced
172 | via linting and static analysis. You can find the exact details of what these points are by reading
173 | the corresponding [linter configuration](../.golangci.yaml). In order to get appropriate warnings
174 | while editing the code you can
175 | [configure your editor](https://github.com/golangci/golangci-lint/#editor-integration) to use
176 | `golangci-lint` with this project's specific configuration.
177 |
178 | ### Continuous integration
179 |
180 | The quality of this project's code is maintained by running each Pull Request through a continuous
181 | integration pipeline provided by [Travis CI](https://travis-ci.com/Helcaraxan/gomod). A passing
182 | build is required for each Pull Request before it can be merged.
183 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug encountered while using 'gomod'.
4 | labels: bug
5 | ---
6 |
7 | # Bug report
8 |
9 | ## Checklist
10 |
11 | - [ ] Your description contains all necessary context to understand and reproduce the bug you are reporting.
12 | - [ ] You have filled in the three sections below and deleted their corresponding placeholders texts.
13 |
14 | ## Description
15 |
16 | - 🐛 _Detail the bug you are reporting, not the fix you'd like to see._
17 |
18 | ## Suggested way of addressing this issue
19 |
20 | _If you have any suggestions on:_
21 |
22 | - 🔬 _How to fix your bug_
23 |
24 | _here's where you describe them!_
25 |
26 | ## Additional information
27 |
28 | _If there's further context or information necessary to reproduce your bug please provide it here._
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an improvement or a new feature for 'gomod'.
4 | labels: "feature request"
5 | ---
6 |
7 | # Feature request
8 |
9 | ## Checklist
10 |
11 | - [ ] The feature / improvement you are suggesting overlaps with the purpose of `gomod`: facilitating the management of Go modules and their dependencies.
12 | - [ ] You have examined various alternatives to the new feature / improvement you are suggesting and are describing the results in the description below.
13 | - [ ] You have filled in the three sections below and deleted their corresponding placeholders texts.
14 |
15 | ## Description
16 |
17 | - 💡 _Describe the problem you want this project to solve via a new feature, not the feature itself._
18 | - 📈 _Describe the improvement you'd like to see, not how you want to implement it._
19 |
20 | ## Implementation suggestions
21 |
22 | _If you have any suggestions on:_
23 |
24 | - 🔮 _Which feature would solve your problem_
25 | - 🔧 _How you would implement the improvement you are seeking_
26 |
27 | _here's where you describe them!_
28 |
29 | ## Additional information
30 |
31 | _If there's further context or information necessary to understand your problem or demonstrate the
32 | need for an improvement then specify those here. Things like an example project or code can
33 | sometimes go a long way in improving understanding._
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Pull Request
2 |
3 | ## Checklist
4 |
5 |
8 |
9 | - [ ] Changes are lint-free. You can check this by running `./ci/lint.sh` or by opening a draft PR to trigger the continuous integration pipeline.
10 | - [ ] All tests pass. You can check this by running `./ci/test.sh` or by opening a draft PR to trigger the continuous integration pipeline.
11 | - [ ] If relevant, tests have been updated or added to ensure your changes will not be reverted or broken by future PRs.
12 | - [ ] If relevant, the project's documentation has been updated or extended.
13 | - [ ] If relevant, information has been added to the [release-notes document](../RELEASE_NOTES.md).
14 | - [ ] You have filled in the two sections below and deleted their corresponding placeholders texts.
15 |
16 | ## Summary
17 |
18 | _Explain what the goal of your PR is, why it is needed and how it achieves the described goal._
19 |
20 | ## Tests
21 |
22 | _Describe if the changes your are making require the modification of existing tests or the addition
23 | of new tests. If you honestly think no tests are required please explain why._
24 |
--------------------------------------------------------------------------------
/.github/workflows/premerge.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Premerge
3 |
4 | on: # yamllint disable rule:truthy
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 | branches:
10 | - master
11 |
12 | jobs:
13 | lint:
14 | name: Lint
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - name: Install Go
18 | uses: actions/setup-go@v1
19 | with:
20 | go-version: 1.15
21 | - name: Install Python
22 | uses: actions/setup-python@v1
23 | - name: Check out repository
24 | uses: actions/checkout@v2
25 | with:
26 | fetch-depth: 0
27 | persist-credentials: false
28 | - name: Run linters
29 | run: ./ci/lint.sh
30 |
31 | test:
32 | name: Test
33 | runs-on: ubuntu-20.04
34 | steps:
35 | - name: Install Go
36 | uses: actions/setup-go@v1
37 | with:
38 | go-version: 1.15
39 | - name: Check out repository
40 | uses: actions/checkout@v2
41 | with:
42 | persist-credentials: false
43 | - name: Test & publish code-coverage
44 | uses: paambaati/codeclimate-action@v2.6.0
45 | env:
46 | CC_TEST_REPORTER_ID: ef07ead9fa11867e3688cde45f90e10ba0fddc35793f2003bbf2140a10904e0e
47 | with:
48 | coverageCommand: ./ci/test.sh
49 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: release
3 |
4 | on: # yamllint disable rule:truthy
5 | push:
6 | tags:
7 | - "v*"
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-20.04
12 | steps:
13 | - name: Install Go
14 | uses: actions/setup-go@v1
15 | with:
16 | go-version: 1.15
17 | - name: Checkout
18 | uses: actions/checkout@v2
19 | with:
20 | fetch-depth: 0
21 | - name: Determine tag
22 | run: |
23 | TAG=${GITHUB_REF#refs/*/}
24 | echo "RELEASE_VERSION=${TAG%%-*}" >> ${GITHUB_ENV}
25 | - name: Run GoReleaser
26 | uses: goreleaser/goreleaser-action@v2
27 | with:
28 | version: latest
29 | args: release --rm-dist --release-notes=docs/release-notes/${{env.RELEASE_VERSION}}.md
30 | env:
31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .idea/
3 | .vscode/
4 |
5 | # Build and test artefacts
6 | bin/
7 | dist/
8 | c.out
9 | gomod-*
10 | graph.dot
11 | graph.pdf
12 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | # For full documentation of the configuration options please
4 | # see: https://github.com/golangci/golangci-lint#config-file.
5 |
6 | # options for analysis running
7 | run:
8 | # timeout for analysis, e.g. 30s, 5m, default is 1m
9 | deadline: 30s
10 |
11 | # which dirs to skip: they won't be analyzed;
12 | # can use regexp here: generated.*, regexp is applied on full path;
13 | # default value is empty list, but next dirs are always skipped independently
14 | # from this option's value:
15 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
16 | skip-dirs:
17 |
18 | # which files to skip: they will be analyzed, but issues from them
19 | # won't be reported. Default value is empty list, but there is
20 | # no need to include all autogenerated files, we confidently recognize
21 | # autogenerated files. If it's not please let us know.
22 | skip-files:
23 |
24 |
25 | # output configuration options
26 | output:
27 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
28 | format: colored-line-number
29 |
30 |
31 | # linters that we should / shouldn't run
32 | linters:
33 | disable-all: true
34 | enable:
35 | - dupl
36 | - errcheck
37 | - goconst
38 | - gocritic
39 | - gocyclo
40 | - gofmt
41 | - goimports
42 | - golint
43 | - govet
44 | - lll
45 | - misspell
46 | - nakedret
47 | - unparam
48 | - unused
49 |
50 |
51 | # all available settings of specific linters, we can set an option for
52 | # a given linter even if we deactivate that same linter at runtime
53 | linters-settings:
54 | goconst:
55 | # minimal length of string constant, 3 by default
56 | min-len: 5
57 | # minimal occurrences count to trigger, 3 by default
58 | min-occurrences: 3
59 | gocritic:
60 | # which checks should be enabled; can't be combined with 'disabled-checks';
61 | # default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref
62 | # ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef
63 | # unlambda unslice rangeValCopy defaultCaseOrder];
64 | # all checks list: https://github.com/go-critic/checkers
65 | # disabled for now - hugeParam
66 | enabled-checks:
67 | - appendAssign
68 | - assignOp
69 | - boolExprSimplify
70 | - builtinShadow
71 | - captLocal
72 | - caseOrder
73 | - commentedOutImport
74 | - defaultCaseOrder
75 | - dupArg
76 | - dupBranchBody
77 | - dupCase
78 | - dupSubExpr
79 | - elseif
80 | - emptyFallthrough
81 | - ifElseChain
82 | - importShadow
83 | - indexAlloc
84 | - methodExprCall
85 | - nestingReduce
86 | - offBy1
87 | - ptrToRefParam
88 | - regexpMust
89 | - singleCaseSwitch
90 | - sloppyLen
91 | - switchTrue
92 | - typeSwitchVar
93 | - typeUnparen
94 | - underef
95 | - unlambda
96 | - unnecessaryBlock
97 | - unslice
98 | - valSwap
99 | - wrapperFunc
100 | - yodaStyleExpr
101 | gocyclo:
102 | # minimal code complexity to report, 30 by default (but we recommend 10-20)
103 | min-complexity: 15
104 | goimports:
105 | # put imports beginning with prefix after 3rd-party packages;
106 | # it's a comma-separated list of prefixes
107 | local-prefixes: github.com/Helcaraxan/gomod
108 | lll:
109 | # max line length, lines longer will be reported. Default is 120.
110 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option
111 | line-length: 150
112 | maligned:
113 | # print struct with more effective memory layout or not, false by default
114 | suggest-new: true
115 | misspell:
116 | # Correct spellings using locale preferences for US or UK.
117 | # Default is to use a neutral variety of English.
118 | # Setting locale to US will correct the British spelling of 'colour' to 'color'.
119 | locale: UK
120 | nakedret:
121 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
122 | max-func-lines: 0 # Warn on all naked returns.
123 |
124 |
125 | # rules to deal with reported isues
126 | issues:
127 | # List of regexps of issue texts to exclude, empty list by default.
128 | # But independently from this option we use default exclude patterns,
129 | # it can be disabled by `exclude-use-default: false`. To list all
130 | # excluded by default patterns execute `golangci-lint run --help`
131 | exclude:
132 |
133 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
134 | max-per-linter: 0
135 |
136 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
137 | max-same-issues: 0
138 |
139 | # Show only new issues: if there are unstaged changes or untracked files,
140 | # only those changes are analyzed, else only changes in HEAD~ are analyzed.
141 | # It's a super-useful option for integration of golangci-lint into existing
142 | # large codebase. It's not practical to fix all existing issues at the moment
143 | # of integration: much better don't allow issues in new code.
144 | # Default is false.
145 | new: false
146 |
147 | # Show only new issues created after git revision `REV`. If set to "" lint the
148 | # entire specified packages.
149 | new-from-rev: ""
150 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | ---
2 | project_name: gomod
3 | before:
4 | hooks:
5 | - go mod download
6 | builds:
7 | - env:
8 | - CGO_ENABLED=0
9 | flags:
10 | - -trimpath
11 | ldflags:
12 | - -s -w
13 | - -X main.version={{.Version}}
14 | - -X main.date={{.CommitDate}}
15 | - -X main.commit={{.ShortCommit}}
16 | goos:
17 | - linux
18 | - windows
19 | - darwin
20 | goarch:
21 | - amd64
22 | - arm64
23 | goarm:
24 | - 6
25 | - 7
26 | mod_timestamp: "{{.CommitTimestamp}}"
27 | archives:
28 | - name_template: "{{.Binary}}-{{.Os}}-{{.Arch}}"
29 | format: binary
30 | release:
31 | name_template: "v{{.Version}}"
32 | github:
33 | owner: Helcaraxan
34 | name: gomod
35 | prerelease: auto
36 |
--------------------------------------------------------------------------------
/.mdlrc:
--------------------------------------------------------------------------------
1 | git_recurse true
2 | style "./mdl_style.rb"
--------------------------------------------------------------------------------
/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | braces:
4 | min-spaces-inside: 0
5 | max-spaces-inside: 0
6 | min-spaces-inside-empty: -1
7 | max-spaces-inside-empty: -1
8 | brackets:
9 | min-spaces-inside: 0
10 | max-spaces-inside: 0
11 | min-spaces-inside-empty: -1
12 | max-spaces-inside-empty: -1
13 | colons:
14 | max-spaces-before: 0
15 | max-spaces-after: 1
16 | commas:
17 | max-spaces-before: 0
18 | min-spaces-after: 1
19 | max-spaces-after: 1
20 | comments:
21 | level: warning
22 | require-starting-space: true
23 | min-spaces-from-content: 1
24 | comments-indentation:
25 | level: warning
26 | document-end: disable
27 | document-start:
28 | level: warning
29 | present: true
30 | empty-lines:
31 | max: 2
32 | max-start: 0
33 | max-end: 1
34 | empty-values:
35 | forbid-in-block-mappings: false
36 | forbid-in-flow-mappings: true
37 | hyphens:
38 | max-spaces-after: 1
39 | indentation:
40 | spaces: consistent
41 | indent-sequences: true
42 | check-multi-line-strings: false
43 | key-duplicates: enable
44 | key-ordering: disable
45 | line-length:
46 | max: 150
47 | allow-non-breakable-words: true
48 | allow-non-breakable-inline-mappings: true
49 | new-line-at-end-of-file: enable
50 | new-lines:
51 | type: unix
52 | trailing-spaces: disable
53 | truthy:
54 | level: warning
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Duco van Amstel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ci/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # vim: set tabstop=2 shiftwidth=2 expandtab
3 | set -u -e -o pipefail
4 | [[ -n ${DEBUG:-} ]] && set -x
5 |
6 | if [[ "$(uname -s)" != "Linux" ]]; then
7 | echo "This script is only intended to be run on Linux as the used CLI tools might not be available or differ in their semantics."
8 | exit 1
9 | fi
10 |
11 | readonly PROJECT_ROOT="$(dirname "${BASH_SOURCE[0]}")/.."
12 | cd "${PROJECT_ROOT}"
13 |
14 | # Ensure linter versions are specified or set the default values.
15 | readonly GOLANGCI_VERSION="${GOLANGCI_VERSION:-"1.27.0"}"
16 | readonly MARKDOWNLINT_VERSION="${MARKDOWNLINT_VERSION:-"0.9.0"}"
17 | readonly SHELLCHECK_VERSION="${SHELLCHECK_VERSION:-"0.7.1"}"
18 | readonly SHFMT_VERSION="${SHFMT_VERSION:-"3.1.1"}"
19 | readonly YAMLLINT_VERSION="${YAMLLINT_VERSION:-"1.23.0"}"
20 |
21 | # Retrieve linters if necessary.
22 | mkdir -p "${PWD}/bin"
23 | export PATH="${PWD}/bin:${PATH}"
24 |
25 | ## golangci-lint
26 | if [[ -z "$(command -v golangci-lint)" ]] || ! grep "${GOLANGCI_VERSION}" <<<"$(golangci-lint --version)"; then
27 | echo "Installing golangci-lint@${GOLANGCI_VERSION}."
28 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY="golang-ci" bash -s -- -b "${PWD}/bin" "v${GOLANGCI_VERSION}"
29 | else
30 | echo "Found installed golangci-lint@${GOLANGCI_VERSION}."
31 | fi
32 |
33 | ## shellcheck
34 | if [[ -z "$(command -v shellcheck)" ]] || ! grep "${SHELLCHECK_VERSION}" <<<"$(shellcheck --version)"; then
35 | echo "Installing shellcheck@${SHELLCHECK_VERSION}."
36 | curl -LSs "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" |
37 | tar --extract --xz --strip-components=1 --directory="${PWD}/bin" "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
38 | else
39 | echo "Found installed shellcheck@${SHELLCHECK_VERSION}."
40 | fi
41 |
42 | # shfmt
43 | if [[ -z "$(command -v shfmt)" ]] || ! grep "${SHFMT_VERSION}" <<<"$(shfmt -version)"; then
44 | echo "Installing shfmt."
45 | GOBIN="${PWD}/bin" go install -modfile=tools.mod "mvdan.cc/sh/v3/cmd/shfmt"
46 | else
47 | echo "Found installed shfmt."
48 | fi
49 |
50 | ## markdownlint
51 | if [[ -z "$(command -v mdl)" ]] || ! grep "${MARKDOWNLINT_VERSION}" <<<"$(mdl --version)"; then
52 | echo "Installing mdl@${MARKDOWNLINT_VERSION}."
53 | mkdir -p "${HOME}/.ruby"
54 | export GEM_HOME="${HOME}/.ruby"
55 | gem install mdl "--version=${MARKDOWNLINT_VERSION}" --bindir=./bin
56 | else
57 | echo "Found installed mdl@${MARKDOWNLINT_VERSION}."
58 | fi
59 |
60 | ## yamllint
61 | if [[ -z "$(command -v yamllint)" ]] || ! grep "${YAMLLINT_VERSION}" <<<"$(yamllint --version)"; then
62 | echo "Installing yamllint@${YAMLLINT_VERSION}."
63 | pip install "yamllint==${YAMLLINT_VERSION}"
64 | else
65 | echo "Found installed yamllint@${YAMLLINT_VERSION}."
66 | fi
67 |
68 | # Run linters.
69 | echo "Ensuring that generated Go code is being kept up to date."
70 | go generate ./...
71 | git diff --exit-code --quiet '*.go' || (
72 | echo "Please run 'go generate ./...' to update the generated Go code."
73 | false
74 | )
75 |
76 | echo "Linting YAML files."
77 | yamllint --strict --config-file=./.yamllint.yaml .
78 |
79 | echo "Linting Go source code."
80 | golangci-lint run ./...
81 |
82 | echo "Ensuring that 'go.mod' and 'go.sum' are being kept up to date."
83 | go mod tidy
84 | git diff --exit-code --quiet go.mod go.sum || (
85 | echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."
86 | false
87 | )
88 |
89 | echo "Linting Bash scripts."
90 | declare -a shell_files
91 | while read -r file; do
92 | shell_files+=("${file}")
93 | done <<<"$(shfmt -f .)"
94 | shellcheck --external-sources --shell=bash --severity=style "${shell_files[@]}"
95 |
96 | shell_failure=0
97 | readonly shell_vim_directives="# vim: set tabstop=2 shiftwidth=2 expandtab"
98 | for shell_file in "${shell_files[@]}"; do
99 | if ! grep -q "^${shell_vim_directives}$" "${shell_file}"; then
100 | echo "'${shell_file}' is missing the compulsory VIm directives: ${shell_vim_directives}"
101 | shell_failure=1
102 | fi
103 | done
104 | if ((shell_failure == 1)); then
105 | echo "Errors were detected while linting shell scripts."
106 | exit 1
107 | fi
108 |
109 | echo "Checking the formatting of Bash scripts."
110 | shfmt -i 2 -s -w -d .
111 |
112 | echo "Linting Markdown files."
113 | mdl .
114 |
--------------------------------------------------------------------------------
/ci/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # vim: set tabstop=2 shiftwidth=2 expandtab
3 | set -e -u -o pipefail
4 |
5 | readonly project_root="$(dirname "${BASH_SOURCE[0]}")/.."
6 | cd "${project_root}"
7 |
8 | readonly linux_binary="gomod-linux-x86_64"
9 | readonly darwin_binary="gomod-darwin-x86_64"
10 | readonly windows_binary="gomod-windows-x86_64.exe"
11 |
12 | # Ensure we know which release version we are aiming for.
13 | if [[ -z ${RELEASE_VERSION:-} || ! ${RELEASE_VERSION} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
14 | echo "Please specify the targeted version via the RELEASE_VERSION environment variable (e.g. '0.5.0')."
15 | exit 1
16 | fi
17 |
18 | readonly tag="v${RELEASE_VERSION}"
19 |
20 | # Ensure we have a GitHub authentication token available.
21 | if [[ -z ${GITHUB_API_TOKEN:-} ]]; then
22 | echo "Please specify a GitHub API token with appropriate permissions via the GITHUB_API_TOKEN environment variable."
23 | exit 1
24 | fi
25 |
26 | # Ensure the release tag does not yet exist.
27 | if git tag -l | grep --quiet "^${tag}$"; then
28 | echo "The targeted releaes '${RELEASE_VERSION}' already seems to exist. Aborting."
29 | exit 1
30 | fi
31 |
32 | # Ensure we have a release-notes file for the targeted version.
33 | release_notes_file="docs/release-notes/v${RELEASE_VERSION}.md"
34 | if [[ ! -f ${release_notes_file} ]]; then
35 | echo "There is no release-notes files available for version '${RELEASE_VERSION}' in the 'docs/release-notes' folder."
36 | exit 1
37 | fi
38 | release_description="$(cat "${release_notes_file}")"
39 |
40 | readonly build_time="$(date -u +'%Y-%m-%d %H:%M:%S')"
41 |
42 | printf "\nBuilding release binaries..."
43 | printf "\n- Linux..."
44 | GOARCH=amd64 GOOS=linux go build -o "${linux_binary}" -ldflags "-X 'main.toolVersion=${tag}' -X 'main.toolDate=${build_time}'" .
45 | printf " DONE\n- MacOS..."
46 | GOARCH=amd64 GOOS=darwin go build -o "${darwin_binary}" -ldflags "-X 'main.toolVersion=${tag}' -X 'main.toolDate=${build_time}'" .
47 | printf " DONE\n- Windows..."
48 | GOARCH=amd64 GOOS=windows go build -o "${windows_binary}" -ldflags "-X 'main.toolVersion=${tag}' -X 'main.toolDate=${build_time}'" .
49 | echo " DONE"
50 |
51 | echo "--- RELEASE DESCRIPTION ---"
52 | echo "${release_description}"
53 | echo "--- RELEASE DESCRIPTION ---"
54 | echo ""
55 | echo "Are you sure you want to create the '${RELEASE_VERSION}' release with the description above?"
56 |
57 | read -r -p "(Y/n) " -n 1
58 | echo ""
59 | if [[ ! ${REPLY} =~ ^[Yy]$ ]]; then
60 | echo "Aborting."
61 | exit 1
62 | fi
63 |
64 | readonly release_notes="$(mktemp)"
65 | echo "{
66 | \"tag_name\": \"${tag}\",
67 | \"name\": \"${tag}\",
68 | \"body\": \"$(awk '{ printf "%s\\n", $0 }' <<<"${release_description//\"/\\\"}")\"
69 | }" >"${release_notes}"
70 |
71 | printf "\nTagging and pushing release commit..."
72 | git tag --force "${tag}"
73 | git push --quiet --force origin "${tag}"
74 | echo " DONE"
75 |
76 | printf "\nCreating the GitHub release..."
77 | readonly create_response="$(
78 | curl --silent \
79 | --data "@${release_notes}" \
80 | --header "Authorization: token ${GITHUB_API_TOKEN}" \
81 | --header "Content-Type: application/json" \
82 | https://api.github.com/repos/Helcaraxan/gomod/releases
83 | )"
84 |
85 | readonly release_name="$(jq --raw-output '.name' <<<"${create_response}")"
86 | readonly release_url="$(jq --raw-output '.url' <<<"${create_response}")"
87 | readonly upload_url="$(jq --raw-output '.upload_url' <<<"${create_response}")"
88 |
89 | if [[ -z ${release_name} || -z ${release_url} || -z ${upload_url} ]]; then
90 | echo " FAILED"
91 | echo ""
92 | printf "ERROR: It appears that the release creation failed. The API's response was:\n%s\n\n" "${create_response}"
93 | exit 1
94 | fi
95 | echo " DONE"
96 | echo "The release can be found at ${release_url}."
97 |
98 | echo ""
99 | echo "Uploading release assets..."
100 | readonly release_assets=(
101 | "${linux_binary}"
102 | "${darwin_binary}"
103 | "${windows_binary}"
104 | )
105 | for asset in "${release_assets[@]}"; do
106 | echo "- ${asset}..."
107 | upload_response="$(
108 | curl --progress-bar \
109 | --data-binary "@${asset}" \
110 | --header "Authorization: token ${GITHUB_API_TOKEN}" \
111 | --header "Content-Type: application/octet-stream" \
112 | "${upload_url%%\{*}?name=${asset}"
113 | )"
114 | upload_state="$(jq --raw-output '.state' <<<"${upload_response}")"
115 | if [[ ${upload_state} != uploaded ]]; then
116 | echo ""
117 | printf "ERROR: It appears that the upload of asset ${asset} failed. The API's response was:\n%s\n\n" "${upload_response}"
118 | exit 1
119 | fi
120 | unset upload_response
121 | unset upload_state
122 | done
123 |
124 | echo ""
125 | echo "The ${RELEASE_VERSION} release was successfully created."
126 |
127 | rm -f "${release_assets[@]}"
128 |
--------------------------------------------------------------------------------
/ci/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # vim: set tabstop=2 shiftwidth=2 expandtab
3 | set -e -u -o pipefail
4 |
5 | PROJECT_ROOT="$(dirname "${BASH_SOURCE[0]}")/.."
6 | cd "${PROJECT_ROOT}"
7 |
8 | # Run all the Go tests with the race detector and generate coverage.
9 | printf "\nRunning Go test...\n"
10 | go test -v -race -coverprofile c.out -coverpkg=all ./...
11 |
--------------------------------------------------------------------------------
/docs/release-notes/next.md:
--------------------------------------------------------------------------------
1 | # RELEASE-NUMBER
2 |
3 | ## High-level overview
4 |
5 | ## Bug fixes
6 |
7 | ## New features
8 |
9 | ## Breaking changes
10 |
--------------------------------------------------------------------------------
/docs/release-notes/template.md:
--------------------------------------------------------------------------------
1 | # RELEASE-NUMBER
2 |
3 | ## High-level overview
4 |
5 | ## Bug fixes
6 |
7 | ## New features
8 |
9 | ## Breaking changes
10 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.1.0.md:
--------------------------------------------------------------------------------
1 | # v0.1.0
2 |
3 | ## High-level overview
4 |
5 | - Created CLI binary with the graph command.
6 | - Support for filtering the dependency graph on various criteria.
7 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.2.0.md:
--------------------------------------------------------------------------------
1 | # v0.2.0
2 |
3 | ## High-level overview
4 |
5 | - Redesigned the flags for gomod graph and their semantics for ease-of-use.
6 | - Added the gomod analyse command to generate statistics about a module's dependencies.
7 | - Numerous internal improvements.
8 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.2.1.md:
--------------------------------------------------------------------------------
1 | # v0.2.1
2 |
3 | ## High-level overview
4 |
5 | - Only require the dot tool when running the gomod graph command.
6 | - New developments in the project now have to go through CI before being merged.
7 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.3.0.md:
--------------------------------------------------------------------------------
1 | # v0.3.0
2 |
3 | ## High-level overview
4 |
5 | - Added the gomod reveal command to find and highlight hidden replaces in (indirect) module dependencies.
6 | - Added the analyze alias for gomod analyse.
7 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.3.1.md:
--------------------------------------------------------------------------------
1 | # v0.3.1
2 |
3 | ## High-level overview
4 |
5 | - Fixed graph printing which was broken between 0.2.1 and 0.3.0.
6 | - Fixed printing of non-versioned (local) hidden replace statements.
7 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.4.0.md:
--------------------------------------------------------------------------------
1 | # v0.4.0
2 |
3 | ## High-level overview
4 |
5 | - The presence of the `.dot` tool is now only required when specifying the `-V | --visual` flag to
6 | `gomod graph`.
7 | - Support for node clustering in generated `.dot` files.
8 | - More fine-grained control over graph generation via the new `--style` flag of `gomod graph`.
9 |
10 | ## New features
11 |
12 | - Generated `.dot` graphs are now using box nodes rather than the default ellipse style to reduce
13 | the size of the generated image files and improve readability.
14 | - Specifying formatting options for image generation via `gomod graph` or the underlying library
15 | functions is now done via a dedicated configuration type.
16 | - The `printer.PrintToDot` function can now generate improved layouts for dependency graphs via the
17 | use of node clustering, tightly packing modules that share common reverse dependencies together.
18 | This can result in significant improvements for larger depdendency graphs (_e.g. the PNG image of
19 | the full dependency graph for the [kubernetes](https://github.com/kubernetes/kubernetes) project
20 | has 42% less pixels and has a ~7x smaller binary size_).
21 |
22 | ## Breaking changes
23 |
24 | - The `depgraph.DepGraph` and it's associated methods have been reworked to facilitate
25 | reproducibility through determinism, meaning their signatures have changed. Both a `NodeReference`
26 | and `NodeMap` type have been introduced.
27 | - The `depgraph.GetDepGraph()` method no longer takes a boolean to indicate what output should be
28 | forwarded from the invocations of underlying tools. Instead this is inferred from the level
29 | configured on the `logrus.Logger` instance argument that it takes. `logrus.WarnLevel` and below
30 | are considered the same as `--quiet`, `logrus.DebugLevel` and above are equivalent to `--verbose`.
31 | - Output behaviour for the invocation of underlying tools has slightly changed:
32 | - By default only their `stderr` will be forwarded to the terminal output.
33 | - If the `-q | --quiet` flag is passed neither their `stderr`, not their `stdout` will be
34 | forwarded.
35 | - If the `-v | --verbose` flag is passed both `stderr` and `stdout` will be forwarded.
36 |
37 | In any case the full output of these invocations can be found in the debug logs.
38 | - The `Visual` field of the `printer.PrinterConfig` type has been replaced by `Style` which is a
39 | pointer to a nested `printer.StyleOptions` type. The `printer.Print` method will generate an
40 | image if and only if `Style` has a non-`nil` value.
41 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.5.0.md:
--------------------------------------------------------------------------------
1 | # v0.5.0
2 |
3 | ## High-level overview
4 |
5 | - A significant number of types, methods and functions have been renamed in preparation for a
6 | future `v1.0.0` release. These renames aim to create a more coherent interface for the
7 | functionalities exposed by the `depgraph` package.
8 |
9 | ## New features
10 |
11 | - The `depgraph.DepGraph` type now exposes a `RemoveDependency` method allowing to remove a given
12 | module including any edges starting or ending at this module.
13 | - The new `lib/modules` package exposes methods to retrieve various levels of module information.
14 | - The `depgraph.DepAnalysis` type now also contains information about the update backlog time of
15 | a module's dependencies. This reflects the timespan between the timestamp of the used version of a
16 | dependency and the timestamp of the newest available update.
17 |
18 | ## Breaking changes
19 |
20 | - Package split: the `depgraph.Module` and `depgraph.ModuleError` types have been extracted to a
21 | separate `lib/modules` package in preparation for future work that will expand the configurability
22 | of information loading to support new features.
23 | - Type renames:
24 | - `depgraph.Node` has been renamed to `depgraph.Dependency` after the pre-existing type of that
25 | name has been removed in the `v0.4.0` release.
26 | - `depgraph.NodeReference` has been renamed to `depgraph.DependencyReference`.
27 | - `depgraph.NodeMap` has been renamed to `depgraph.DependencyMap` and the associated
28 | `NewNodeMap()` function has accordingly been renamed to `NewDependencyMap()`.
29 | - The `depgraph.DepGraph` type's methods have changed:
30 | - `Main()` has been removed in favour of direct access to a field with the same name.
31 | - `Nodes()` has been removed in favour of direct access to a field named `Dependencies`.
32 | - `Node()` has been renamed to `GetDependency()`.
33 | - `AddNode()` has been renamed to `AddDependency` and now only returns a `*Dependency` instead of
34 | also a `bool`. The returned `value` is `nil` if the module passed as parameter could not be
35 | added.
36 | - The `depgraph.DependencyFilter` type's `Dependency` field has been renamed to `Module`.
37 | - The `depgraph.NewDepGraph()` function now also takes the path where the contained module lives.
38 | - The `depgraph.GetDepGraph()` function now also takes a relative or absolute path to the directory
39 | where the targeted Go module lives.
40 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.6.0.md:
--------------------------------------------------------------------------------
1 | # v0.6.0
2 |
3 | ## High-level overview
4 |
5 | - The generated dependency graphs now reflect the true import-based dependency paths for non-module
6 | projects instead of the artificial dependencies injected by the module system to provide
7 | reproducibility. Indirect dependencies induced by the module system are represented by dashed
8 | lines.
9 |
10 | ## New features
11 |
12 | - A new `gomod version` enables the printing of the current version of the `gomod` tool.
13 | - The `gomod graph` command has a whole new query syntax that makes it much easier to trim down the
14 | generated graph to the exact content that you need. Check the updated
15 | [README section](README.md#gomod-graph) for more details.
16 | - The `gomod graph` command now support querying the package-level import graph in addition to the
17 | already available module-level dependency graph. Simply use the new `--package | -p` flag to move
18 | into package mode.
19 | - The `--verbose` flag now takes optional string arguments to toggle debug output for specific parts
20 | of `gomod`'s logic.
21 |
22 | ## Breaking changes
23 |
24 | - All library functionalities have been, for now, moved into the `internal` tree while further work
25 | is being done on the actual API and end-functionality of `gomod`.
26 | - The `--shared` and `--dependencies` flags on the `gomod graph` command have been removed with the
27 | arrival of the new query syntax.
28 | - The `gomod completion` command has been removed with the introduction of the new query syntax
29 | which would require much more complex logic for enabling autocomplete.
30 | - `gomod` no longer wraps the invocation of `dot`. To get an image as output simply pipe `gomod`'s
31 | output into the `dot` binary on the command-line. This means that the `--format` and `--visual`
32 | flags have been removed and the the `--style` flag no longer implies `--visual`.
33 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.6.1.md:
--------------------------------------------------------------------------------
1 | # v0.6.1
2 |
3 | ## Bug fixes
4 |
5 | - Fixed a potential panic when using the `--annotate` flag in combination with the `--packages` flag
6 | in the `gomod graph` command.
7 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.6.2.md:
--------------------------------------------------------------------------------
1 | # v0.6.2
2 |
3 | ## Bug fixes
4 |
5 | - Removed panic on non-test path queries at package-level.
6 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.7.0.md:
--------------------------------------------------------------------------------
1 | # v0.7.0
2 |
3 | ## Bug fixes
4 |
5 | - Not all test-only dependent packages were adequately marked as such. This has been addressed and
6 | any packages only imported when building tests are now appropriately recognised as such.
7 |
8 | ## New features
9 |
10 | - If no query is specified for `gomod graph` then it will return the full dependency graph by
11 | default.
12 | - Nodes in the generated DOT graphs are now coloured based on their (parent) module's name.
13 | Test-only dependencies are distinguishable by a lighter colour-palette than core dependencies.
14 | Similary edges reflecting test-only dependencies are now marked with a distinct colour just as
15 | indirect module dependencies are reflected by dashed lines instead of continous ones.
16 |
17 | ## Breaking changes
18 |
19 | - The query syntax for paths has been modified in favour of using glob-based matching. As a result
20 | the `foo/...` prefix-matching is no longer recognised. Instead the more flexible `foo/**` can be
21 | used which also allows for middle-of-path wildcards such as `foo/**/bar/*`.
22 |
--------------------------------------------------------------------------------
/docs/release-notes/v0.7.1.md:
--------------------------------------------------------------------------------
1 | # v0.7.1
2 |
3 | ## Bug fixes
4 |
5 | - [#115] Enforce module-mode when running `go list` in projects using vendoring.
6 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Helcaraxan/gomod
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/bmatcuk/doublestar/v3 v3.0.0
7 | github.com/spf13/cobra v1.1.1
8 | github.com/stretchr/testify v1.6.1
9 | go.uber.org/zap v1.16.0
10 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
11 | )
12 |
--------------------------------------------------------------------------------
/images/dependency-chains.dot:
--------------------------------------------------------------------------------
1 | strict digraph {
2 | node [shape=box,style="rounded,filled"]
3 | start=0
4 | concentrate=true
5 | "github.com/Helcaraxan/gomod" [fontcolor="0.000 0.000 0.000",fillcolor="0.824 0.753 1.000"]
6 | "github.com/bketelsen/crypt" [fontcolor="0.000 0.000 0.000",fillcolor="0.702 0.260 1.000",label=v0.0.3-0.20200106085610-5cbc8cc4026c>]
7 | "github.com/hashicorp/consul/api" [fontcolor="0.000 0.000 0.000",fillcolor="0.965 0.207 1.000",label=v1.1.0>]
8 | "github.com/hashicorp/memberlist" [fontcolor="0.000 0.000 0.000",fillcolor="0.302 0.340 1.000",label=v0.1.3>]
9 | "github.com/hashicorp/serf" [fontcolor="0.000 0.000 0.000",fillcolor="0.635 0.273 1.000",label=v0.8.2>]
10 | "github.com/prometheus/client_golang" [fontcolor="0.000 0.000 0.000",fillcolor="0.102 0.380 1.000",label=v0.9.3>]
11 | "github.com/prometheus/common" [fontcolor="0.000 0.000 0.000",fillcolor="0.384 0.323 1.000",label=v0.4.0>]
12 | "github.com/prometheus/tsdb" [fontcolor="0.000 0.000 0.000",fillcolor="0.945 0.211 1.000",label=v0.7.1>]
13 | "github.com/sirupsen/logrus" [fontcolor="0.000 0.000 0.000",fillcolor="0.592 0.282 1.000",label=v1.2.0>]
14 | "github.com/spf13/cast" [fontcolor="0.000 0.000 0.000",fillcolor="0.843 0.231 1.000",label=v1.3.0>]
15 | "github.com/spf13/cobra" [fontcolor="0.000 0.000 0.000",fillcolor="0.988 0.704 1.000",label=v1.1.1>]
16 | "github.com/spf13/viper" [fontcolor="0.000 0.000 0.000",fillcolor="0.533 0.293 1.000",label=v1.7.0>]
17 | "github.com/stretchr/testify" [fontcolor="0.000 0.000 0.000",fillcolor="0.569 0.829 1.000",label=v1.6.1>]
18 | "go.uber.org/atomic" [fontcolor="0.000 0.000 0.000",fillcolor="0.949 0.715 1.000",label=v1.6.0>]
19 | "go.uber.org/multierr" [fontcolor="0.000 0.000 0.000",fillcolor="0.345 0.896 1.000",label=v1.5.0>]
20 | "go.uber.org/zap" [fontcolor="0.000 0.000 0.000",fillcolor="1.000 0.700 1.000",label=v1.16.0>]
21 | "github.com/Helcaraxan/gomod" -> "github.com/spf13/cobra" [label=<v1.1.1>]
22 | "github.com/Helcaraxan/gomod" -> "github.com/stretchr/testify" [minlen=4,label=<v1.6.1>]
23 | "github.com/Helcaraxan/gomod" -> "go.uber.org/zap" [minlen=3,label=<v1.16.0>]
24 | "github.com/bketelsen/crypt" -> "github.com/hashicorp/consul/api" [color=lightblue,label=<v1.1.0>]
25 | "github.com/hashicorp/consul/api" -> "github.com/hashicorp/serf" [color=lightblue,label=<v0.8.2>]
26 | "github.com/hashicorp/consul/api" -> "github.com/stretchr/testify" [minlen=2,label=<v1.3.0>]
27 | "github.com/hashicorp/memberlist" -> "github.com/stretchr/testify" [label=<v1.2.2>]
28 | "github.com/hashicorp/serf" -> "github.com/hashicorp/memberlist" [color=lightblue,label=<v0.1.3>]
29 | "github.com/hashicorp/serf" -> "github.com/stretchr/testify" [minlen=2,style=dashed,label=<v1.3.0>]
30 | "github.com/prometheus/client_golang" -> "github.com/prometheus/common" [minlen=2,color=lightblue,label=<v0.4.0>]
31 | "github.com/prometheus/client_golang" -> "github.com/prometheus/tsdb" [minlen=3,color=lightblue,label=<v0.7.1>]
32 | "github.com/prometheus/common" -> "github.com/prometheus/client_golang" [color=lightblue,label=<v0.9.1>]
33 | "github.com/prometheus/common" -> "github.com/sirupsen/logrus" [minlen=4,color=lightblue,label=<v1.2.0>]
34 | "github.com/prometheus/tsdb" -> "github.com/prometheus/client_golang" [minlen=2,color=lightblue,label=<v0.9.1>]
35 | "github.com/prometheus/tsdb" -> "github.com/prometheus/common" [minlen=3,style=dashed,color=lightblue,label=<v0.0.0-20181113130724-41aa239b4cce>]
36 | "github.com/prometheus/tsdb" -> "github.com/stretchr/testify" [minlen=6,style=dashed,label=<v1.2.2>]
37 | "github.com/sirupsen/logrus" -> "github.com/stretchr/testify" [label=<v1.2.2>]
38 | "github.com/spf13/cast" -> "github.com/stretchr/testify" [label=<v1.2.2>]
39 | "github.com/spf13/cobra" -> "github.com/spf13/viper" [color=lightblue,label=<v1.7.0>]
40 | "github.com/spf13/viper" -> "github.com/bketelsen/crypt" [color=lightblue,label=<v0.0.3-0.20200106085610-5cbc8cc4026c>]
41 | "github.com/spf13/viper" -> "github.com/prometheus/client_golang" [style=dashed,color=lightblue,label=<v0.9.3>]
42 | "github.com/spf13/viper" -> "github.com/spf13/cast" [color=lightblue,label=<v1.3.0>]
43 | "github.com/spf13/viper" -> "github.com/stretchr/testify" [minlen=6,label=<v1.3.0>]
44 | "github.com/spf13/viper" -> "go.uber.org/atomic" [minlen=3,style=dashed,label=<v1.4.0>]
45 | "github.com/spf13/viper" -> "go.uber.org/multierr" [minlen=2,style=dashed,label=<v1.1.0>]
46 | "github.com/spf13/viper" -> "go.uber.org/zap" [style=dashed,label=<v1.10.0>]
47 | "go.uber.org/atomic" -> "github.com/stretchr/testify" [label=<v1.3.0>]
48 | "go.uber.org/multierr" -> "github.com/stretchr/testify" [minlen=2,label=<v1.3.0>]
49 | "go.uber.org/multierr" -> "go.uber.org/atomic" [label=<v1.6.0>]
50 | "go.uber.org/zap" -> "github.com/stretchr/testify" [minlen=2,label=<v1.4.0>]
51 | "go.uber.org/zap" -> "go.uber.org/atomic" [minlen=2,label=<v1.6.0>]
52 | "go.uber.org/zap" -> "go.uber.org/multierr" [label=<v1.5.0>]
53 | }
54 |
--------------------------------------------------------------------------------
/images/dependency-chains.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Helcaraxan/gomod/7a27bfad203a324a521be71ad0581205840a635c/images/dependency-chains.jpg
--------------------------------------------------------------------------------
/images/full.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Helcaraxan/gomod/7a27bfad203a324a521be71ad0581205840a635c/images/full.jpg
--------------------------------------------------------------------------------
/images/shared-dependencies.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Helcaraxan/gomod/7a27bfad203a324a521be71ad0581205840a635c/images/shared-dependencies.jpg
--------------------------------------------------------------------------------
/internal/analysis/analysis_test.go:
--------------------------------------------------------------------------------
1 | package analysis
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 | "time"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 |
14 | "github.com/Helcaraxan/gomod/internal/depgraph"
15 | "github.com/Helcaraxan/gomod/internal/testutil"
16 | )
17 |
18 | func Test_DistributionCountToPercentage(t *testing.T) {
19 | inputDistribution := []int{1, 10, 5, 4}
20 | expectedOutputNoGrouping := []float64{0.05, 0.5, 0.25, 0.20}
21 | expectedOutputThreeGroup := []float64{0.8, 0.2}
22 | assert.Equal(t, expectedOutputNoGrouping, distributionCountToPercentage(inputDistribution, 1))
23 | assert.Equal(t, expectedOutputThreeGroup, distributionCountToPercentage(inputDistribution, 3))
24 | }
25 |
26 | func Test_DistributionToLines(t *testing.T) {
27 | inputDistribution := []float64{0.05, 0.48, 0.35, 0.12}
28 | expectedOutput := []string{
29 | "||||||",
30 | "__",
31 | "_",
32 | "_#####",
33 | "_",
34 | "_###_",
35 | "_",
36 | "_#",
37 | }
38 | assert.Equal(t, expectedOutput, distributionToLines(inputDistribution, 5))
39 | }
40 |
41 | func Test_RotateDistributionLines(t *testing.T) {
42 | input := []string{
43 | "||||||",
44 | "_##_",
45 | "_",
46 | "_#####",
47 | "__",
48 | }
49 | expected := []string{
50 | "| # ",
51 | "| # ",
52 | "|_ # ",
53 | "|# # ",
54 | "|# #_",
55 | "|____",
56 | }
57 | assert.Equal(t, expected, rotateDistributionLines(input, 5), "Should have gotten the expected output")
58 | }
59 |
60 | type testcase struct {
61 | CurrentTime *time.Time `yaml:"now"`
62 | ListModOutput map[string]string `yaml:"go_list_mod_output"`
63 | ListPkgOutput map[string]string `yaml:"go_list_pkg_output"`
64 | GraphOutput string `yaml:"go_graph_output"`
65 |
66 | ExpectedDepAnalysis *DepAnalysis `yaml:"dep_analysis"`
67 | ExpectedPrintOutput string `yaml:"print_output"`
68 | }
69 |
70 | func (c *testcase) GoDriverError() bool { return false }
71 | func (c *testcase) GoListModOutput() map[string]string { return c.ListModOutput }
72 | func (c *testcase) GoListPkgOutput() map[string]string { return c.ListPkgOutput }
73 | func (c *testcase) GoGraphOutput() string { return c.GraphOutput }
74 |
75 | func TestAnalysis(t *testing.T) {
76 | cwd, setupErr := os.Getwd()
77 | require.NoError(t, setupErr)
78 |
79 | // Prepend the testdata directory to the path so we use the fake "go" script.
80 | setupErr = os.Setenv("PATH", filepath.Join(cwd, "..", "internal", "testutil")+":"+os.Getenv("PATH"))
81 | require.NoError(t, setupErr)
82 |
83 | files, setupErr := ioutil.ReadDir(filepath.Join(cwd, "testdata"))
84 | require.NoError(t, setupErr)
85 |
86 | for idx := range files {
87 | file := files[idx]
88 | if file.IsDir() || filepath.Ext(file.Name()) != ".yaml" {
89 | continue
90 | }
91 |
92 | testname := strings.TrimSuffix(file.Name(), ".yaml")
93 | t.Run(testname, func(t *testing.T) {
94 | testDefinition := &testcase{}
95 | testDir := testutil.SetupTestModule(t, filepath.Join(cwd, "testdata", file.Name()), testDefinition)
96 |
97 | if testDefinition.CurrentTime != nil {
98 | testCurrentTimeInjection = testDefinition.CurrentTime
99 | defer func() { testCurrentTimeInjection = nil }()
100 | }
101 |
102 | log := testutil.TestLogger(t)
103 | graph, err := depgraph.GetGraph(log, testDir)
104 | require.NoError(t, err)
105 |
106 | analysis, err := Analyse(log.Log(), graph)
107 | require.NoError(t, err)
108 | assert.Equal(t, testDefinition.ExpectedDepAnalysis, analysis)
109 |
110 | output := &strings.Builder{}
111 | err = analysis.Print(output)
112 | require.NoError(t, err)
113 | assert.Equal(t, testDefinition.ExpectedPrintOutput, output.String())
114 | })
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/DeprecatedUpdate.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep: |
10 | {
11 | "Path": "dep",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0",
14 | "Update": {
15 | "Path": "dep",
16 | "Time": "2018-01-01T00:00:00Z",
17 | "Version": "v1.1.0"
18 | }
19 | }
20 | go_list_pkg_output:
21 | test/...: |
22 | {
23 | "ImportPath": "test",
24 | "Module": {
25 | "Path": "test",
26 | "Main": true
27 | }
28 | }
29 | go_graph_output: |
30 | test dep@v1.0.0
31 | dep_analysis:
32 | module: "test"
33 | direct_dependencies: 1
34 | indirect_dependencies: 0
35 | mean_age: 60000000000ns
36 | max_age: 60000000000ns
37 | age_per_month:
38 | - 1
39 | available_updates: 0
40 | mean_backlog: 0ns
41 | max_backlog: 0ns
42 | backlog_per_month:
43 | mean_reverse_deps: 1
44 | max_reverse_deps: 1
45 | reverse_deps_distribution:
46 | - 0
47 | - 1
48 | print_output: |+
49 | -- Analysis for 'test' --
50 | Dependency counts:
51 | - Direct dependencies: 1
52 | - Indirect dependencies: 0
53 |
54 | Age statistics:
55 | - Mean age of dependencies: 0 month(s) 0 day(s)
56 | - Maximum dependency age: 0 month(s) 0 day(s)
57 | - Age distribution per month:
58 |
59 | 100.00 % |#
60 | |#
61 | |#
62 | |#
63 | |#
64 | |#
65 | |#
66 | |#
67 | |#
68 | |#
69 | 0.00 % |_
70 | 0 1
71 |
72 | Update backlog statistics:
73 | - No available updates. Congratulations you are entirely up-to-date!
74 |
75 | Reverse dependency statistics:
76 | - Mean number of reverse dependencies: 1.00
77 | - Maximum number of reverse dependencies: 1
78 | - Reverse dependency count distribution:
79 |
80 | 100.00 % | #
81 | | #
82 | | #
83 | | #
84 | | #
85 | | #
86 | | #
87 | | #
88 | | #
89 | | #
90 | 0.00 % |___
91 | 0 2
92 |
93 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/MissingTimestamp.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep: |
10 | {
11 | "Path": "dep",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0",
14 | "Update": {
15 | "Path": "dep",
16 | "Time": "2019-01-01T00:00:30Z",
17 | "Version": "v1.1.0"
18 | }
19 | }
20 | no_timestamp: |
21 | {
22 | "Path": "no_timestamp",
23 | "Version": "v0.0.1",
24 | "Update": {
25 | "Path": "no_timestamp",
26 | "Time": "2019-01-01T00:00:30Z",
27 | "Version": "v0.0.2"
28 | }
29 | }
30 | go_list_pkg_output:
31 | test/...: |
32 | {
33 | "ImportPath": "test",
34 | "Module": {
35 | "Path": "test",
36 | "Main": true
37 | }
38 | }
39 |
40 | go_graph_output: |
41 | test dep@v1.0.0
42 | test no_timestamp@v0.0.1
43 | dep_analysis:
44 | module: "test"
45 | direct_dependencies: 2
46 | indirect_dependencies: 0
47 | mean_age: 60000000000ns
48 | max_age: 60000000000ns
49 | age_per_month:
50 | - 1
51 | available_updates: 1
52 | available_updates_direct: 1
53 | mean_backlog: 30000000000ns
54 | max_backlog: 30000000000ns
55 | backlog_per_month:
56 | - 1
57 | mean_reverse_deps: 1
58 | max_reverse_deps: 1
59 | reverse_deps_distribution:
60 | - 0
61 | - 2
62 | print_output: |+
63 | -- Analysis for 'test' --
64 | Dependency counts:
65 | - Direct dependencies: 2
66 | - Indirect dependencies: 0
67 |
68 | Age statistics:
69 | - Mean age of dependencies: 0 month(s) 0 day(s)
70 | - Maximum dependency age: 0 month(s) 0 day(s)
71 | - Age distribution per month:
72 |
73 | 100.00 % |#
74 | |#
75 | |#
76 | |#
77 | |#
78 | |#
79 | |#
80 | |#
81 | |#
82 | |#
83 | 0.00 % |_
84 | 0 1
85 |
86 | Update backlog statistics:
87 | - Number of dependencies with an update: 1 (of which 1 is direct)
88 | - Mean update backlog of dependencies: 0 month(s) 0 day(s)
89 | - Maximum update backlog of dependencies: 0 month(s) 0 day(s)
90 | - Update backlog distribution per month:
91 |
92 | 100.00 % |#
93 | |#
94 | |#
95 | |#
96 | |#
97 | |#
98 | |#
99 | |#
100 | |#
101 | |#
102 | 0.00 % |_
103 | 0 1
104 |
105 | Reverse dependency statistics:
106 | - Mean number of reverse dependencies: 1.00
107 | - Maximum number of reverse dependencies: 1
108 | - Reverse dependency count distribution:
109 |
110 | 100.00 % | #
111 | | #
112 | | #
113 | | #
114 | | #
115 | | #
116 | | #
117 | | #
118 | | #
119 | | #
120 | 0.00 % |___
121 | 0 2
122 |
123 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/NoUpdates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep: |
10 | {
11 | "Path": "dep",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0"
14 | }
15 | go_list_pkg_output:
16 | test/...: |
17 | {
18 | "ImportPath": "test",
19 | "Module": {
20 | "Path": "test",
21 | "Main": true
22 | }
23 | }
24 | go_graph_output: |
25 | test dep@v1.0.0
26 | dep_analysis:
27 | module: "test"
28 | direct_dependencies: 1
29 | indirect_dependencies: 0
30 | mean_age: 60000000000ns
31 | max_age: 60000000000ns
32 | age_per_month:
33 | - 1
34 | available_updates: 0
35 | mean_backlog: 0ns
36 | max_backlog: 0ns
37 | backlog_per_month:
38 | mean_reverse_deps: 1
39 | max_reverse_deps: 1
40 | reverse_deps_distribution:
41 | - 0
42 | - 1
43 | print_output: |+
44 | -- Analysis for 'test' --
45 | Dependency counts:
46 | - Direct dependencies: 1
47 | - Indirect dependencies: 0
48 |
49 | Age statistics:
50 | - Mean age of dependencies: 0 month(s) 0 day(s)
51 | - Maximum dependency age: 0 month(s) 0 day(s)
52 | - Age distribution per month:
53 |
54 | 100.00 % |#
55 | |#
56 | |#
57 | |#
58 | |#
59 | |#
60 | |#
61 | |#
62 | |#
63 | |#
64 | 0.00 % |_
65 | 0 1
66 |
67 | Update backlog statistics:
68 | - No available updates. Congratulations you are entirely up-to-date!
69 |
70 | Reverse dependency statistics:
71 | - Mean number of reverse dependencies: 1.00
72 | - Maximum number of reverse dependencies: 1
73 | - Reverse dependency count distribution:
74 |
75 | 100.00 % | #
76 | | #
77 | | #
78 | | #
79 | | #
80 | | #
81 | | #
82 | | #
83 | | #
84 | | #
85 | 0.00 % |___
86 | 0 2
87 |
88 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/OneDep.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep: |
10 | {
11 | "Path": "dep",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0",
14 | "Update": {
15 | "Path": "dep",
16 | "Time": "2019-01-01T00:00:30Z",
17 | "Version": "v1.1.0"
18 | }
19 | }
20 | go_list_pkg_output:
21 | test/...: |
22 | {
23 | "ImportPath": "test",
24 | "Module": {
25 | "Path": "test",
26 | "Main": true
27 | }
28 | }
29 | go_graph_output: |
30 | test dep@v1.0.0
31 | dep_analysis:
32 | module: "test"
33 | direct_dependencies: 1
34 | indirect_dependencies: 0
35 | mean_age: 60000000000ns
36 | max_age: 60000000000ns
37 | age_per_month:
38 | - 1
39 | available_updates: 1
40 | available_updates_direct: 1
41 | mean_backlog: 30000000000ns
42 | max_backlog: 30000000000ns
43 | backlog_per_month:
44 | - 1
45 | mean_reverse_deps: 1
46 | max_reverse_deps: 1
47 | reverse_deps_distribution:
48 | - 0
49 | - 1
50 | print_output: |+
51 | -- Analysis for 'test' --
52 | Dependency counts:
53 | - Direct dependencies: 1
54 | - Indirect dependencies: 0
55 |
56 | Age statistics:
57 | - Mean age of dependencies: 0 month(s) 0 day(s)
58 | - Maximum dependency age: 0 month(s) 0 day(s)
59 | - Age distribution per month:
60 |
61 | 100.00 % |#
62 | |#
63 | |#
64 | |#
65 | |#
66 | |#
67 | |#
68 | |#
69 | |#
70 | |#
71 | 0.00 % |_
72 | 0 1
73 |
74 | Update backlog statistics:
75 | - Number of dependencies with an update: 1 (of which 1 is direct)
76 | - Mean update backlog of dependencies: 0 month(s) 0 day(s)
77 | - Maximum update backlog of dependencies: 0 month(s) 0 day(s)
78 | - Update backlog distribution per month:
79 |
80 | 100.00 % |#
81 | |#
82 | |#
83 | |#
84 | |#
85 | |#
86 | |#
87 | |#
88 | |#
89 | |#
90 | 0.00 % |_
91 | 0 1
92 |
93 | Reverse dependency statistics:
94 | - Mean number of reverse dependencies: 1.00
95 | - Maximum number of reverse dependencies: 1
96 | - Reverse dependency count distribution:
97 |
98 | 100.00 % | #
99 | | #
100 | | #
101 | | #
102 | | #
103 | | #
104 | | #
105 | | #
106 | | #
107 | | #
108 | 0.00 % |___
109 | 0 2
110 |
111 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/OneDirectOneIndirect.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep1: |
10 | {
11 | "Path": "dep1",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0",
14 | "Update": {
15 | "Path": "dep1",
16 | "Time": "2019-01-01T00:00:30Z",
17 | "Version": "v1.1.0"
18 | }
19 | }
20 | dep2: |
21 | {
22 | "Path": "dep2",
23 | "Time": "2019-01-01T00:00:30Z",
24 | "Version": "v0.1.0",
25 | "Update": {
26 | "Path": "dep2",
27 | "Time": "2019-01-01T00:00:40Z",
28 | "Version": "v0.2.0"
29 | }
30 | }
31 | go_list_pkg_output:
32 | test/...: |
33 | {
34 | "ImportPath": "test",
35 | "Module": {
36 | "Path": "test",
37 | "Main": true
38 | }
39 | }
40 | go_graph_output: |
41 | test dep1@v1.0.0
42 | dep1@v1.0.0 dep2@v0.1.0
43 | dep_analysis:
44 | module: "test"
45 | direct_dependencies: 1
46 | indirect_dependencies: 1
47 | mean_age: 45000000000ns
48 | max_age: 60000000000ns
49 | age_per_month:
50 | - 2
51 | available_updates: 2
52 | available_updates_direct: 1
53 | mean_backlog: 20000000000ns
54 | max_backlog: 30000000000ns
55 | backlog_per_month:
56 | - 2
57 | mean_reverse_deps: 1
58 | max_reverse_deps: 1
59 | reverse_deps_distribution:
60 | - 0
61 | - 2
62 | print_output: |+
63 | -- Analysis for 'test' --
64 | Dependency counts:
65 | - Direct dependencies: 1
66 | - Indirect dependencies: 1
67 |
68 | Age statistics:
69 | - Mean age of dependencies: 0 month(s) 0 day(s)
70 | - Maximum dependency age: 0 month(s) 0 day(s)
71 | - Age distribution per month:
72 |
73 | 100.00 % |#
74 | |#
75 | |#
76 | |#
77 | |#
78 | |#
79 | |#
80 | |#
81 | |#
82 | |#
83 | 0.00 % |_
84 | 0 1
85 |
86 | Update backlog statistics:
87 | - Number of dependencies with an update: 2 (of which 1 is direct)
88 | - Mean update backlog of dependencies: 0 month(s) 0 day(s)
89 | - Maximum update backlog of dependencies: 0 month(s) 0 day(s)
90 | - Update backlog distribution per month:
91 |
92 | 100.00 % |#
93 | |#
94 | |#
95 | |#
96 | |#
97 | |#
98 | |#
99 | |#
100 | |#
101 | |#
102 | 0.00 % |_
103 | 0 1
104 |
105 | Reverse dependency statistics:
106 | - Mean number of reverse dependencies: 1.00
107 | - Maximum number of reverse dependencies: 1
108 | - Reverse dependency count distribution:
109 |
110 | 100.00 % | #
111 | | #
112 | | #
113 | | #
114 | | #
115 | | #
116 | | #
117 | | #
118 | | #
119 | | #
120 | 0.00 % |___
121 | 0 2
122 |
123 |
--------------------------------------------------------------------------------
/internal/analysis/testdata/TwoDeps.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | now: "2019-01-01T00:01:00Z"
3 | go_list_mod_output:
4 | test: |
5 | {
6 | "Path": "test",
7 | "Main": true
8 | }
9 | dep1: |
10 | {
11 | "Path": "dep1",
12 | "Time": "2019-01-01T00:00:00Z",
13 | "Version": "v1.0.0",
14 | "Update": {
15 | "Path": "dep1",
16 | "Time": "2019-01-01T00:00:30Z",
17 | "Version": "v1.1.0"
18 | }
19 | }
20 | dep2: |
21 | {
22 | "Path": "dep2",
23 | "Time": "2019-01-01T00:00:30Z",
24 | "Version": "v0.1.0",
25 | "Update": {
26 | "Path": "dep2",
27 | "Time": "2019-01-01T00:00:40Z",
28 | "Version": "v0.2.0"
29 | }
30 | }
31 | go_list_pkg_output:
32 | test/...: |
33 | {
34 | "ImportPath": "test",
35 | "Module": {
36 | "Path": "test",
37 | "Main": true
38 | }
39 | }
40 | go_graph_output: |
41 | test dep1@v1.0.0
42 | test dep2@v0.1.0
43 | dep_analysis:
44 | module: "test"
45 | direct_dependencies: 2
46 | mean_age: 45000000000ns
47 | max_age: 60000000000ns
48 | age_per_month:
49 | - 2
50 | available_updates: 2
51 | available_updates_direct: 2
52 | mean_backlog: 20000000000ns
53 | max_backlog: 30000000000ns
54 | backlog_per_month:
55 | - 2
56 | mean_reverse_deps: 1
57 | max_reverse_deps: 1
58 | reverse_deps_distribution:
59 | - 0
60 | - 2
61 | print_output: |+
62 | -- Analysis for 'test' --
63 | Dependency counts:
64 | - Direct dependencies: 2
65 | - Indirect dependencies: 0
66 |
67 | Age statistics:
68 | - Mean age of dependencies: 0 month(s) 0 day(s)
69 | - Maximum dependency age: 0 month(s) 0 day(s)
70 | - Age distribution per month:
71 |
72 | 100.00 % |#
73 | |#
74 | |#
75 | |#
76 | |#
77 | |#
78 | |#
79 | |#
80 | |#
81 | |#
82 | 0.00 % |_
83 | 0 1
84 |
85 | Update backlog statistics:
86 | - Number of dependencies with an update: 2 (of which 2 are direct)
87 | - Mean update backlog of dependencies: 0 month(s) 0 day(s)
88 | - Maximum update backlog of dependencies: 0 month(s) 0 day(s)
89 | - Update backlog distribution per month:
90 |
91 | 100.00 % |#
92 | |#
93 | |#
94 | |#
95 | |#
96 | |#
97 | |#
98 | |#
99 | |#
100 | |#
101 | 0.00 % |_
102 | 0 1
103 |
104 | Reverse dependency statistics:
105 | - Mean number of reverse dependencies: 1.00
106 | - Maximum number of reverse dependencies: 1
107 | - Reverse dependency count distribution:
108 |
109 | 100.00 % | #
110 | | #
111 | | #
112 | | #
113 | | #
114 | | #
115 | | #
116 | | #
117 | | #
118 | | #
119 | 0.00 % |___
120 | 0 2
121 |
122 |
--------------------------------------------------------------------------------
/internal/depgraph/colours.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "math/bits"
7 | )
8 |
9 | func hashToColourHSV(hash string, isTest bool) (text string, background string) {
10 | var h byte
11 | for _, b := range []byte(hash) {
12 | h ^= bits.RotateLeft8(uint8(b), int(b))
13 | }
14 | hue := float32(uint8(h)) / float32(math.MaxUint8)
15 | satVar := float32(uint8(h^0xff)) / float32(math.MaxUint8)
16 |
17 | text = "0.000 0.000 0.000"
18 | sat := 0.7 + 0.3*satVar
19 | if isTest {
20 | sat = 0.2 + 0.2*satVar
21 | } else if hue < 0.10 || (hue > 0.6 && hue < 0.8) {
22 | text = "0.000 0.000 1.000"
23 | }
24 | return text, fmt.Sprintf("%.3f %.3f 1.000", hue, sat)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/depgraph/deps_mod.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "regexp"
7 | "strings"
8 |
9 | "go.uber.org/zap"
10 |
11 | "github.com/Helcaraxan/gomod/internal/logger"
12 | "github.com/Helcaraxan/gomod/internal/util"
13 | )
14 |
15 | func (g *DepGraph) overlayModuleDependencies(dl *logger.Builder) error {
16 | log := dl.Domain(logger.ModuleDependencyDomain)
17 | log.Debug("Overlaying module-based dependency information over the import dependency graph.")
18 |
19 | raw, _, err := util.RunCommand(log, g.Main.Info.Dir, "go", "mod", "graph")
20 | if err != nil {
21 | return err
22 | }
23 |
24 | for _, depString := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
25 | log.Debug("Parsing dependency", zap.String("reference", depString))
26 | modDep, ok := g.parseDependency(log, depString)
27 | if !ok {
28 | continue
29 | }
30 |
31 | log.Debug(
32 | "Overlaying module dependency.",
33 | zap.String("version", modDep.targetVersion),
34 | zap.String("source", modDep.source.Name()),
35 | zap.String("target", modDep.target.Name()),
36 | )
37 | err = g.Graph.AddEdge(modDep.source, modDep.target)
38 | if err != nil {
39 | return err
40 | }
41 | modDep.source.VersionConstraints[modDep.target.Hash()] = VersionConstraint{
42 | Source: modDep.sourceVersion,
43 | Target: modDep.targetVersion,
44 | }
45 | }
46 |
47 | if err := g.markIndirects(log); err != nil {
48 | return err
49 | }
50 |
51 | return nil
52 | }
53 |
54 | type moduleDependency struct {
55 | source *Module
56 | sourceVersion string
57 | target *Module
58 | targetVersion string
59 | }
60 |
61 | func (g *DepGraph) parseDependency(log *logger.Logger, depString string) (*moduleDependency, bool) {
62 | depContent := depRE.FindStringSubmatch(depString)
63 | if len(depContent) == 0 {
64 | log.Warn("Skipping ill-formed line in 'go mod graph' output.", zap.String("line", depString))
65 | return nil, false
66 | }
67 |
68 | sourceName, sourceVersion := depContent[1], depContent[2]
69 | targetName, targetVersion := depContent[3], depContent[4]
70 |
71 | source, ok := g.getModule(sourceName)
72 | if !ok {
73 | log.Warn("Encountered a dependency edge starting at an unknown module.", zap.String("source", sourceName), zap.String("target", targetName))
74 | return nil, false
75 | }
76 | target, ok := g.getModule(targetName)
77 | if !ok {
78 | log.Warn("Encountered a dependency edge ending at an unknown module.", zap.String("source", sourceName), zap.String("target", targetName))
79 | return nil, false
80 |
81 | }
82 |
83 | if sourceVersion != source.Info.Version {
84 | log.Debug(
85 | "Skipping edge as we are not using the specified source version.",
86 | zap.String("source", sourceName),
87 | zap.String("version", sourceVersion),
88 | zap.String("target", targetName),
89 | )
90 | return nil, false
91 | }
92 | log.Debug(
93 | "Recording module dependency.",
94 | zap.String("source", sourceName),
95 | zap.String("version", sourceVersion),
96 | zap.String("target", targetName),
97 | )
98 |
99 | return &moduleDependency{
100 | source: source,
101 | sourceVersion: sourceVersion,
102 | target: target,
103 | targetVersion: targetVersion,
104 | }, true
105 | }
106 |
107 | func (g *DepGraph) markIndirects(log *logger.Logger) error {
108 | for _, node := range g.Graph.GetLevel(int(LevelModules)).List() {
109 | module := node.(*Module)
110 |
111 | log := log.With(zap.String("module", module.Name()))
112 | log.Debug("Finding indirect dependencies for module.")
113 |
114 | if module.Info.GoMod == "" {
115 | // This occurs when we are under tests and can be skipped safely.
116 | continue
117 | }
118 |
119 | modContent, err := ioutil.ReadFile(module.Info.GoMod)
120 | if err != nil {
121 | log.Error("Failed to read content of go.mod file.", zap.String("path", module.Info.GoMod), zap.Error(err))
122 | return err
123 | }
124 |
125 | indirectDepRE := regexp.MustCompile(`^ ([^\s]+) [^\s]+ // indirect$`)
126 | for _, line := range bytes.Split(modContent, []byte("\n")) {
127 | if m := indirectDepRE.FindSubmatch(line); len(m) == 2 {
128 | log.Debug("Found indirect dependency.", zap.String("consumer", module.Name()), zap.String("dependency", string(m[1])))
129 | module.Indirects[string(m[1])] = true
130 | }
131 | }
132 | }
133 | return nil
134 | }
135 |
--------------------------------------------------------------------------------
/internal/depgraph/deps_pkg.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "strings"
9 |
10 | "go.uber.org/zap"
11 |
12 | "github.com/Helcaraxan/gomod/internal/graph"
13 | "github.com/Helcaraxan/gomod/internal/logger"
14 | "github.com/Helcaraxan/gomod/internal/modules"
15 | "github.com/Helcaraxan/gomod/internal/util"
16 | )
17 |
18 | func (g *DepGraph) buildImportGraph(dl *logger.Builder) error {
19 | log := dl.Domain(logger.PackageInfoDomain)
20 | log.Debug("Building initial dependency graph based on the import graph.")
21 |
22 | err := g.retrieveTransitiveImports(log, []string{fmt.Sprintf("%s/...", g.Main.Info.Path)})
23 | if err != nil {
24 | return err
25 | }
26 |
27 | pkgs := g.Graph.GetLevel(int(LevelPackages))
28 | for _, node := range pkgs.List() {
29 | pkg := node.(*Package)
30 |
31 | imports := pkg.Info.Imports
32 | if pkg.parent.Name() == g.Main.Name() {
33 | imports = append(imports, pkg.Info.TestImports...)
34 | imports = append(imports, pkg.Info.XTestImports...)
35 | }
36 |
37 | for _, imp := range imports {
38 | if isStandardLib(imp) {
39 | continue
40 | }
41 |
42 | targetNode, _ := pkgs.Get(packageHash(imp))
43 | if targetNode == nil {
44 | log.Error("Detected import of unknown package.", zap.String("package", imp))
45 | continue
46 | }
47 |
48 | log.Debug(
49 | "Adding package dependency.",
50 | zap.String("source", pkg.Name()),
51 | zap.String("source-module", pkg.Parent().Name()),
52 | zap.String("target", targetNode.Name()),
53 | zap.String("target-module", targetNode.Parent().Name()),
54 | )
55 | targetPkg := targetNode.(*Package)
56 | if err = g.Graph.AddEdge(pkg, targetPkg); err != nil {
57 | return err
58 | }
59 | }
60 | }
61 |
62 | if err = g.markNonTestDependencies(log); err != nil {
63 | return err
64 | }
65 |
66 | return nil
67 | }
68 |
69 | func (g *DepGraph) markNonTestDependencies(log *logger.Logger) error {
70 | log.Debug("Marking non-test dependencies.")
71 |
72 | var todo []graph.Node
73 | seen := map[string]bool{}
74 |
75 | for _, mainPkg := range g.Main.packages.List() {
76 | if strings.HasSuffix(mainPkg.(*Package).Info.Name, "_test") {
77 | log.Debug("Skipping main module package as it is a test-only package.", zap.String("package", mainPkg.Name()))
78 | continue
79 | }
80 |
81 | todo = append(todo, mainPkg)
82 | seen[mainPkg.Name()] = true
83 | }
84 |
85 | for len(todo) > 0 {
86 | next := todo[0]
87 | todo = todo[1:]
88 |
89 | log.Debug("Marking package as non-test dependency.", zap.String("package", next.Name()))
90 | next.(*Package).isNonTestDependency = true
91 | next.Parent().(*Module).isNonTestDependency = true
92 |
93 | for _, imp := range next.(*Package).Info.Imports {
94 | if isStandardLib(imp) {
95 | continue
96 | }
97 |
98 | dep, err := g.Graph.GetNode(packageHash(imp))
99 | if err != nil {
100 | return err
101 | }
102 |
103 | if !seen[dep.Name()] {
104 | todo = append(todo, dep)
105 | seen[dep.Name()] = true
106 | }
107 | }
108 | }
109 | return nil
110 | }
111 |
112 | func (g *DepGraph) retrieveTransitiveImports(log *logger.Logger, pkgs []string) error {
113 | const maxQueryLength = 950 // This is chosen conservatively to ensure we don't exceed maximum command lengths for 'go list' invocations.
114 |
115 | queued := map[string]bool{}
116 | for len(pkgs) > 0 {
117 | queryLength := 0
118 |
119 | cursor := 0
120 | for {
121 | if cursor == len(pkgs) || queryLength+len(pkgs[cursor]) > maxQueryLength {
122 | break
123 | }
124 | queryLength += len(pkgs[cursor]) + 1
125 | cursor++
126 | }
127 | query := pkgs[:cursor]
128 | pkgs = pkgs[cursor:]
129 |
130 | imports, err := g.retrievePackageInfo(log, query)
131 | if err != nil {
132 | return err
133 | }
134 |
135 | for _, pkg := range imports {
136 | if !queued[pkg] {
137 | queued[pkg] = true
138 | pkgs = append(pkgs, pkg)
139 | }
140 | }
141 | }
142 | return nil
143 | }
144 |
145 | func (g *DepGraph) retrievePackageInfo(log *logger.Logger, pkgs []string) (imports []string, err error) {
146 | stdout, _, err := util.RunCommand(log, g.Main.Info.Dir, "go", append([]string{"list", "-json", "-mod=mod"}, pkgs...)...)
147 | if err != nil {
148 | log.Error("Failed to list imports for packages.", zap.Strings("packages", pkgs), zap.Error(err))
149 | return nil, err
150 | }
151 | dec := json.NewDecoder(bytes.NewReader(stdout))
152 |
153 | for {
154 | pkgInfo := &modules.PackageInfo{}
155 | if err = dec.Decode(pkgInfo); err != nil {
156 | if err == io.EOF {
157 | break
158 | } else {
159 | log.Error("Failed to parse go list output.", zap.Error(err))
160 | return nil, err
161 | }
162 | }
163 | parentModule, ok := g.getModule(pkgInfo.Module.Path)
164 | if !ok {
165 | log.Error("Encountered package in unknown module.", zap.String("package", pkgInfo.ImportPath), zap.String("module", pkgInfo.Module.Path))
166 | continue
167 | }
168 |
169 | pkg := NewPackage(pkgInfo, parentModule)
170 | _ = g.Graph.AddNode(pkg)
171 | log.Debug("Added import information for package", zap.String("package", pkg.Name()), zap.String("module", parentModule.Name()))
172 |
173 | importCandidates := make([]string, len(pkgInfo.Imports))
174 | copy(importCandidates, pkgInfo.Imports)
175 | if parentModule.Name() == g.Main.Name() {
176 | importCandidates = append(importCandidates, pkgInfo.TestImports...)
177 | importCandidates = append(importCandidates, pkgInfo.XTestImports...)
178 | }
179 |
180 | for _, candidate := range importCandidates {
181 | if !isStandardLib(candidate) {
182 | imports = append(imports, candidate)
183 | }
184 | }
185 | }
186 | return imports, nil
187 | }
188 |
189 | func isStandardLib(pkg string) bool {
190 | return !strings.Contains(strings.Split(pkg, "/")[0], ".")
191 | }
192 |
--------------------------------------------------------------------------------
/internal/depgraph/graph.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "github.com/Helcaraxan/gomod/internal/graph"
5 | "github.com/Helcaraxan/gomod/internal/logger"
6 | "github.com/Helcaraxan/gomod/internal/modules"
7 | )
8 |
9 | // DepGraph represents a Go module's dependency graph.
10 | type DepGraph struct {
11 | Path string
12 | Main *Module
13 | Graph *graph.HierarchicalDigraph
14 |
15 | replaces map[string]string
16 | }
17 |
18 | type Level uint8
19 |
20 | const (
21 | LevelModules Level = iota
22 | LevelPackages
23 | )
24 |
25 | func NewGraph(log *logger.Logger, path string, main *modules.ModuleInfo) *DepGraph {
26 | g := &DepGraph{
27 | Path: path,
28 | Graph: graph.NewHierarchicalDigraph(log),
29 | replaces: map[string]string{},
30 | }
31 | g.Main = g.AddModule(main)
32 | return g
33 | }
34 |
35 | func (g *DepGraph) getModule(name string) (*Module, bool) {
36 | if replaced, ok := g.replaces[name]; ok {
37 | name = replaced
38 | }
39 | node, err := g.Graph.GetNode(moduleHash(name))
40 | if err != nil {
41 | return nil, false
42 | }
43 | return node.(*Module), true
44 | }
45 |
46 | func (g *DepGraph) AddModule(module *modules.ModuleInfo) *Module {
47 | if module == nil {
48 | return nil
49 | } else if node, _ := g.Graph.GetNode(moduleHash(module.Path)); node != nil {
50 | return node.(*Module)
51 | }
52 |
53 | newModule := NewModule(module)
54 |
55 | _ = g.Graph.AddNode(newModule)
56 | if module.Replace != nil {
57 | g.replaces[module.Replace.Path] = module.Path
58 | }
59 | return newModule
60 | }
61 |
--------------------------------------------------------------------------------
/internal/depgraph/module.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/Helcaraxan/gomod/internal/graph"
8 | "github.com/Helcaraxan/gomod/internal/modules"
9 | )
10 |
11 | // Module represents a module in a Go module's dependency graph.
12 | type Module struct {
13 | Info *modules.ModuleInfo
14 | Indirects map[string]bool
15 | VersionConstraints map[string]VersionConstraint
16 |
17 | predecessors graph.NodeRefs
18 | successors graph.NodeRefs
19 |
20 | packages graph.NodeRefs
21 | isNonTestDependency bool
22 | }
23 |
24 | type VersionConstraint struct {
25 | Source string
26 | Target string
27 | }
28 |
29 | func NewModule(info *modules.ModuleInfo) *Module {
30 | return &Module{
31 | Info: info,
32 | Indirects: map[string]bool{},
33 | VersionConstraints: map[string]VersionConstraint{},
34 | predecessors: graph.NewNodeRefs(),
35 | successors: graph.NewNodeRefs(),
36 | packages: graph.NewNodeRefs(),
37 | }
38 | }
39 |
40 | // Name of the module represented by this Dependency in the Graph instance.
41 | func (m *Module) Name() string {
42 | return m.Info.Path
43 | }
44 |
45 | func (m *Module) Hash() string {
46 | return moduleHash(m.Info.Path)
47 | }
48 |
49 | func (m *Module) String() string {
50 | return fmt.Sprintf("%s, preds: [%s], succs: [%s]", m.Hash(), m.predecessors, m.successors)
51 | }
52 |
53 | func moduleHash(name string) string {
54 | return "module " + name
55 | }
56 |
57 | // SelectedVersion corresponds to the version of the dependency represented by this Dependency which
58 | // was selected for use.
59 | func (m *Module) SelectedVersion() string {
60 | if m.Info.Replace != nil {
61 | return m.Info.Replace.Version
62 | }
63 | return m.Info.Version
64 | }
65 |
66 | // Timestamp returns the time corresponding to the creation of the version at which this dependency
67 | // is used.
68 | func (m *Module) Timestamp() *time.Time {
69 | if m.Info.Replace != nil {
70 | return m.Info.Replace.Time
71 | }
72 | return m.Info.Time
73 | }
74 |
75 | func (m *Module) Parent() graph.Node {
76 | return nil
77 | }
78 |
79 | func (m *Module) Predecessors() *graph.NodeRefs {
80 | return &m.predecessors
81 | }
82 |
83 | func (m *Module) Successors() *graph.NodeRefs {
84 | return &m.successors
85 | }
86 |
87 | func (m *Module) Children() *graph.NodeRefs {
88 | return &m.packages
89 | }
90 |
91 | func (m *Module) NodeAttributes(annotate bool) []string {
92 | var annotations []string
93 |
94 | text, background := hashToColourHSV(m.Hash(), m.isTestDependency())
95 | annotations = append(annotations, fmt.Sprintf(`fontcolor="%s"`, text), fmt.Sprintf(`fillcolor="%s"`, background))
96 |
97 | if annotate && m.SelectedVersion() != "" {
98 | var replacement string
99 | if m.Info.Replace != nil {
100 | replacement = m.Info.Replace.Path + "
"
101 | }
102 | annotations = append(
103 | annotations,
104 | fmt.Sprintf("label=<%s
%s%s>", m.Name(), replacement, m.SelectedVersion()),
105 | )
106 | }
107 | return annotations
108 | }
109 |
110 | func (m *Module) EdgeAttributes(target graph.Node, annotate bool) []string {
111 | targetModule := target.(*Module)
112 |
113 | var annotations []string
114 | if m.Indirects[target.Name()] {
115 | annotations = append(annotations, "style=dashed") //nolint:misspell
116 | }
117 | if target.(testAnnotated).isTestDependency() {
118 | annotations = append(annotations, "color=lightblue") //nolint:misspell
119 | }
120 | if c, ok := m.VersionConstraints[targetModule.Hash()]; ok && annotate {
121 | annotations = append(annotations, fmt.Sprintf("label=<%s>", c.Target))
122 | }
123 | return annotations
124 | }
125 |
126 | var _ testAnnotated = &Module{}
127 |
128 | func (m *Module) isTestDependency() bool {
129 | return !m.isNonTestDependency
130 | }
131 |
--------------------------------------------------------------------------------
/internal/depgraph/package.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/Helcaraxan/gomod/internal/graph"
7 | "github.com/Helcaraxan/gomod/internal/modules"
8 | )
9 |
10 | // Package represents a single Go package in a dependency graph.
11 | type Package struct {
12 | Info *modules.PackageInfo
13 |
14 | predecessors graph.NodeRefs
15 | successors graph.NodeRefs
16 |
17 | parent *Module
18 | isNonTestDependency bool
19 | }
20 |
21 | func NewPackage(info *modules.PackageInfo, parent *Module) *Package {
22 | return &Package{
23 | Info: info,
24 | predecessors: graph.NewNodeRefs(),
25 | successors: graph.NewNodeRefs(),
26 | parent: parent,
27 | }
28 | }
29 |
30 | // Name returns the import path of the package and not the value declared inside the package with
31 | // the 'package' statement.
32 | func (p *Package) Name() string {
33 | return p.Info.ImportPath
34 | }
35 |
36 | func (p *Package) Hash() string {
37 | return packageHash(p.Info.ImportPath)
38 | }
39 |
40 | func (p *Package) String() string {
41 | return fmt.Sprintf("%s, module: %s, preds: [%s], succs: [%s]", p.Hash(), p.parent.Name(), p.predecessors, p.successors)
42 | }
43 |
44 | func packageHash(name string) string { return "package " + name }
45 |
46 | func (p *Package) Predecessors() *graph.NodeRefs {
47 | return &p.predecessors
48 | }
49 |
50 | func (p *Package) Successors() *graph.NodeRefs {
51 | return &p.successors
52 | }
53 |
54 | func (p *Package) Children() *graph.NodeRefs {
55 | return nil
56 | }
57 |
58 | func (p *Package) Parent() graph.Node {
59 | return p.parent
60 | }
61 |
62 | func (p *Package) NodeAttributes(annotate bool) []string {
63 | var annotations []string
64 |
65 | text, background := hashToColourHSV(p.Parent().Hash(), p.isTestDependency())
66 | annotations = append(annotations, fmt.Sprintf(`fontcolor="%s"`, text), fmt.Sprintf(`fillcolor="%s"`, background))
67 |
68 | return annotations
69 | }
70 |
71 | func (p *Package) EdgeAttributes(target graph.Node, annotate bool) []string {
72 | return nil
73 | }
74 |
75 | func (p *Package) isTestDependency() bool {
76 | return !p.isNonTestDependency
77 | }
78 |
--------------------------------------------------------------------------------
/internal/depgraph/parser.go:
--------------------------------------------------------------------------------
1 | package depgraph
2 |
3 | import (
4 | "os"
5 | "regexp"
6 |
7 | "go.uber.org/zap"
8 |
9 | "github.com/Helcaraxan/gomod/internal/graph"
10 | "github.com/Helcaraxan/gomod/internal/logger"
11 | "github.com/Helcaraxan/gomod/internal/modules"
12 | )
13 |
14 | var depRE = regexp.MustCompile(`^([^@\s]+)@?([^@\s]+)? ([^@\s]+)@([^@\s]+)$`)
15 |
16 | // GetGraph will return the dependency graph for the Go module that can be found at the specified
17 | // path.
18 | func GetGraph(dl *logger.Builder, path string) (*DepGraph, error) {
19 | if dl == nil {
20 | dl = logger.NewBuilder(os.Stderr)
21 | }
22 | log := dl.Domain(logger.GraphDomain)
23 | log.Debug("Creating dependency graph.")
24 |
25 | mainModule, moduleInfo, err := modules.GetDependencies(dl.Domain(logger.ModuleInfoDomain), path)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | g := NewGraph(log, path, mainModule)
31 | for _, module := range moduleInfo {
32 | g.AddModule(module)
33 | }
34 |
35 | if err = g.buildImportGraph(dl); err != nil {
36 | return nil, err
37 | }
38 |
39 | if err = g.overlayModuleDependencies(dl); err != nil {
40 | return nil, err
41 | }
42 |
43 | var roots []graph.Node
44 | for _, module := range g.Graph.GetLevel(0).List() {
45 | if module.Predecessors().Len() == 0 && module.Hash() != g.Main.Hash() {
46 | roots = append(roots, module)
47 | }
48 | }
49 |
50 | for len(roots) > 0 {
51 | next := roots[0]
52 | roots = roots[1:]
53 |
54 | for _, child := range next.Successors().List() {
55 | if child.Predecessors().Len() == 1 {
56 | roots = append(roots, child)
57 | }
58 | }
59 |
60 | log.Debug("Removing module as it not connected to the final graph.", zap.String("dependency", next.Name()))
61 | if err = g.Graph.DeleteNode(next.Hash()); err != nil {
62 | return nil, err
63 | }
64 | }
65 |
66 | return g, nil
67 | }
68 |
--------------------------------------------------------------------------------
/internal/graph/graph.go:
--------------------------------------------------------------------------------
1 | package graph
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "go.uber.org/zap"
8 |
9 | "github.com/Helcaraxan/gomod/internal/logger"
10 | )
11 |
12 | var (
13 | ErrNilGraph = errors.New("cannot operate on nil-graph")
14 | ErrNilNode = errors.New("cannot operate on nil-node")
15 | ErrNodeAlreadyExists = errors.New("node with identical hash already exists in graph")
16 | ErrNodeNotFound = errors.New("node not found")
17 | ErrEdgeSelf = errors.New("self-edges are not allowed")
18 | ErrEdgeCrossLevel = errors.New("edges not allowed between nodes of different hierarchical levels")
19 | )
20 |
21 | type graphErr struct {
22 | err error
23 | ctx string
24 | }
25 |
26 | func (e graphErr) Error() string {
27 | return fmt.Sprintf("%s: %v", e.ctx, e.err)
28 | }
29 |
30 | func (e graphErr) Unwrap() error {
31 | return e.err
32 | }
33 |
34 | type HierarchicalDigraph struct {
35 | log *logger.Logger
36 | members NodeRefs
37 | }
38 |
39 | func NewHierarchicalDigraph(log *logger.Logger) *HierarchicalDigraph {
40 | return &HierarchicalDigraph{
41 | log: log,
42 | members: NewNodeRefs(),
43 | }
44 | }
45 |
46 | func (g HierarchicalDigraph) GetNode(hash string) (Node, error) {
47 | n, _ := g.members.Get(hash)
48 | if n == nil {
49 | return nil, &graphErr{
50 | err: ErrNodeNotFound,
51 | ctx: fmt.Sprintf("node hash %q", hash),
52 | }
53 | }
54 | return n, nil
55 | }
56 |
57 | func (g *HierarchicalDigraph) AddNode(node Node) error {
58 | if g == nil {
59 | return ErrNilGraph
60 | } else if nodeIsNil(node) {
61 | return ErrNilNode
62 | }
63 | g.log.Debug("Adding node to graph.", zap.Stringer("node", node))
64 |
65 | if n, _ := g.members.Get(node.Hash()); n != nil {
66 | return &graphErr{
67 | err: ErrNodeAlreadyExists,
68 | ctx: fmt.Sprintf("node hash %q", node.Hash()),
69 | }
70 | }
71 |
72 | if p := node.Parent(); !nodeIsNil(p) {
73 | if n, _ := g.members.Get(p.Hash()); nodeIsNil(n) {
74 | return &graphErr{
75 | err: ErrNodeNotFound,
76 | ctx: fmt.Sprintf("node hash %q", node.Hash()),
77 | }
78 | }
79 | p.Children().Add(node)
80 | }
81 | g.members.Add(node)
82 |
83 | return nil
84 | }
85 |
86 | func (g *HierarchicalDigraph) DeleteNode(hash string) error {
87 | if g == nil {
88 | return ErrNilGraph
89 | }
90 | g.log.Debug("Deleting node from graph.", zap.String("hash", hash))
91 | g.log.AddIndent()
92 | defer g.log.RemoveIndent()
93 |
94 | target, _ := g.members.Get(hash)
95 | if target == nil {
96 | return &graphErr{
97 | err: ErrNodeNotFound,
98 | ctx: fmt.Sprintf("node hash %q", hash),
99 | }
100 | }
101 |
102 | for _, pred := range target.Predecessors().List() {
103 | g.disconnectNodeFromTarget(pred, target)
104 | }
105 |
106 | for _, succ := range target.Successors().List() {
107 | g.disconnectNodeFromTarget(target, succ)
108 | }
109 |
110 | g.deleteNode(target)
111 |
112 | for !nodeIsNil(target) {
113 | p := target.Parent()
114 | if nodeIsNil(p) || p.Children().Len() > 0 {
115 | break
116 | }
117 |
118 | g.log.AddIndent()
119 | defer g.log.RemoveIndent()
120 |
121 | if err := g.DeleteNode(p.Hash()); err != nil {
122 | return err
123 | }
124 | target = target.Parent()
125 | }
126 | return nil
127 | }
128 |
129 | func (g *HierarchicalDigraph) AddEdge(src Node, dst Node) error {
130 | if g == nil {
131 | return ErrNilGraph
132 | } else if nodeIsNil(src) || nodeIsNil(dst) {
133 | return ErrNilNode
134 | }
135 |
136 | if _, w := g.members.Get(src.Hash()); w == 0 {
137 | return &graphErr{
138 | err: ErrNodeNotFound,
139 | ctx: fmt.Sprintf("node hash %q", src.Hash()),
140 | }
141 | } else if _, w := g.members.Get(dst.Hash()); w == 0 {
142 | return &graphErr{
143 | err: ErrNodeNotFound,
144 | ctx: fmt.Sprintf("node hash %q", dst.Hash()),
145 | }
146 | }
147 |
148 | if nodeDepth(src) != nodeDepth(dst) {
149 | return &graphErr{
150 | err: ErrEdgeCrossLevel,
151 | ctx: fmt.Sprintf("node %q (%d) - node %q (%d)", src.Hash(), nodeDepth(src), dst.Hash(), nodeDepth(dst)),
152 | }
153 | }
154 |
155 | for {
156 | if nodeIsNil(src) || nodeIsNil(dst) || src.Hash() == dst.Hash() {
157 | break
158 | }
159 |
160 | g.log.Debug("Adding edge to graph.", zap.String("source-hash", src.Hash()), zap.String("target-hash", dst.Hash()))
161 | src.Successors().Add(dst)
162 | dst.Predecessors().Add(src)
163 |
164 | src = src.Parent()
165 | dst = dst.Parent()
166 | g.log.AddIndent()
167 | defer g.log.RemoveIndent()
168 | }
169 | return nil
170 | }
171 |
172 | func (g *HierarchicalDigraph) DeleteEdge(src Node, dst Node) error {
173 | if g == nil {
174 | return ErrNilGraph
175 | } else if nodeIsNil(src) || nodeIsNil(dst) {
176 | return ErrNilNode
177 | }
178 |
179 | if _, w := g.members.Get(src.Hash()); w == 0 {
180 | return &graphErr{
181 | err: ErrNodeNotFound,
182 | ctx: fmt.Sprintf("node hash %q", src.Hash()),
183 | }
184 | } else if _, w := g.members.Get(dst.Hash()); w == 0 {
185 | return &graphErr{
186 | err: ErrNodeNotFound,
187 | ctx: fmt.Sprintf("node hash %q", dst.Hash()),
188 | }
189 | }
190 |
191 | g.disconnectNodeFromTarget(src, dst)
192 |
193 | for {
194 | g.log.AddIndent()
195 | defer g.log.RemoveIndent()
196 |
197 | src = src.Parent()
198 | dst = dst.Parent()
199 | if nodeIsNil(src) || nodeIsNil(dst) || src.Hash() == dst.Hash() {
200 | break
201 | }
202 |
203 | g.log.Debug("Unregistring edge from node parents.", zap.String("source-hash", src.Hash()), zap.String("target-hash", dst.Hash()))
204 | src.Successors().Delete(dst.Hash())
205 | dst.Predecessors().Delete(src.Hash())
206 | }
207 | return nil
208 | }
209 |
210 | func (g HierarchicalDigraph) GetLevel(level int) NodeRefs {
211 | refs := NewNodeRefs()
212 | for _, n := range g.members.nodeList {
213 | if nodeDepth(n) == level {
214 | refs.Add(n)
215 | }
216 | }
217 | return refs
218 | }
219 |
220 | func (g HierarchicalDigraph) disconnectNodeFromTarget(n Node, target Node) {
221 | g.log.Debug("Disconnecting nodes.", zap.String("source-hash", n.Hash()), zap.String("target-hash", target.Hash()))
222 | g.log.AddIndent()
223 | defer g.log.RemoveIndent()
224 |
225 | if n.Children() != nil && n.Children().Len() > 0 {
226 | for _, child := range n.Children().List() {
227 | g.disconnectNodeFromTarget(child, target)
228 | }
229 | }
230 |
231 | for _, succ := range n.Successors().List() {
232 | if isChild(succ, target) {
233 | g.log.Debug("Deleting edge from graph.", zap.String("source-hash", n.Hash()), zap.String("target-hash", succ.Hash()))
234 | n.Successors().Wipe(succ.Hash())
235 | succ.Predecessors().Wipe(n.Hash())
236 | g.log.Debug("New successors.", zap.String("hash", n.Hash()), zap.Stringer("succs", n.Successors()))
237 | }
238 | }
239 | }
240 |
241 | func (g HierarchicalDigraph) deleteNode(n Node) {
242 | if n.Children() != nil && n.Children().Len() > 0 {
243 | g.log.AddIndent()
244 | for _, child := range n.Children().List() {
245 | g.deleteNode(child)
246 | }
247 | g.log.RemoveIndent()
248 | }
249 |
250 | g.log.Debug("Removing node from members list.", zap.String("hash", n.Hash()))
251 | g.members.Delete(n.Hash())
252 | if p := n.Parent(); !nodeIsNil(p) {
253 | p.Children().Delete(n.Hash())
254 | }
255 | }
256 |
257 | func isChild(n Node, p Node) bool {
258 | if nodeIsNil(p) {
259 | return false
260 | }
261 | for !nodeIsNil(n) {
262 | if n.Hash() == p.Hash() {
263 | return true
264 | }
265 | n = n.Parent()
266 | }
267 | return false
268 | }
269 |
--------------------------------------------------------------------------------
/internal/graph/graph_test.go:
--------------------------------------------------------------------------------
1 | package graph
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 |
10 | "github.com/Helcaraxan/gomod/internal/testutil"
11 | )
12 |
13 | func TestGraphNodes(t *testing.T) {
14 | var g *HierarchicalDigraph
15 |
16 | n := newTestNode("test-node", nil)
17 | nc1 := newTestNode("test-node-child-1", n)
18 | nc2 := newTestNode("test-node-child-2", n)
19 |
20 | t.Run("NilGraph", func(t *testing.T) {
21 | assert.Equal(t, ErrNilGraph, g.AddNode(n))
22 | assert.Equal(t, ErrNilGraph, g.DeleteNode(n.name))
23 | })
24 |
25 | g = NewHierarchicalDigraph(testutil.TestLogger(t).Log())
26 |
27 | t.Run("AddNode", func(t *testing.T) {
28 | assert.Equal(t, ErrNilNode, g.AddNode(nil))
29 |
30 | nf := newTestNode("non-member", nil)
31 | nfc := newTestNode("non-member-child", nf)
32 | assert.True(t, errors.Is(g.AddNode(nfc), ErrNodeNotFound))
33 |
34 | assert.NoError(t, g.AddNode(n))
35 | assert.Error(t, g.AddNode(n))
36 |
37 | assert.NoError(t, g.AddNode(nc1))
38 | assert.NoError(t, g.AddNode(nc2))
39 | })
40 |
41 | t.Run("GetNode", func(t *testing.T) {
42 | r, err := g.GetNode(n.name)
43 | assert.NoError(t, err)
44 | assert.Equal(t, n, r)
45 |
46 | l0 := g.GetLevel(0)
47 | assert.Equal(t, 1, l0.Len(), l0)
48 | m, _ := l0.Get(n.name)
49 | assert.Equal(t, n, m)
50 |
51 | l1 := g.GetLevel(1)
52 | assert.Equal(t, 2, l1.Len(), l1)
53 | m, _ = l1.Get(nc1.name)
54 | assert.Equal(t, nc1, m)
55 | m, _ = l1.Get(nc2.name)
56 | assert.Equal(t, nc2, m)
57 | })
58 |
59 | t.Run("DeleteNode", func(t *testing.T) {
60 | assert.NoError(t, g.DeleteNode(nc2.name))
61 | assert.True(t, errors.Is(g.DeleteNode(nc2.name), ErrNodeNotFound))
62 |
63 | assert.NoError(t, g.DeleteNode(n.name), g.members)
64 | _, err := g.GetNode(nc1.name)
65 | assert.True(t, errors.Is(err, ErrNodeNotFound))
66 | })
67 | }
68 |
69 | func TestGraphEdges(t *testing.T) {
70 | var g *HierarchicalDigraph
71 |
72 | t.Run("NilGraph", func(t *testing.T) {
73 | assert.Equal(t, ErrNilGraph, g.AddEdge(nil, nil))
74 | assert.Equal(t, ErrNilGraph, g.DeleteEdge(nil, nil))
75 | })
76 |
77 | g = NewHierarchicalDigraph(testutil.TestLogger(t).Log())
78 |
79 | n1 := newTestNode("test-node-1", nil)
80 | n2 := newTestNode("test-node-2", nil)
81 | nc1 := newTestNode("test-node-child-1", n1)
82 | nc2 := newTestNode("test-node-child-2", n2)
83 |
84 | require.NoError(t, g.AddNode(n1))
85 | require.NoError(t, g.AddNode(n2))
86 | require.NoError(t, g.AddNode(nc1))
87 | require.NoError(t, g.AddNode(nc2))
88 |
89 | t.Run("NilNodes", func(t *testing.T) {
90 | assert.Equal(t, ErrNilNode, g.AddEdge(nil, nil))
91 | assert.Equal(t, ErrNilNode, g.AddEdge(n1, nil))
92 | assert.Equal(t, ErrNilNode, g.AddEdge(nil, n2))
93 |
94 | assert.Equal(t, ErrNilNode, g.DeleteEdge(nil, nil))
95 | assert.Equal(t, ErrNilNode, g.DeleteEdge(n1, nil))
96 | assert.Equal(t, ErrNilNode, g.DeleteEdge(nil, n2))
97 | })
98 |
99 | t.Run("AddEdge", func(t *testing.T) {
100 | nf := newTestNode("non-member", nil)
101 | assert.True(t, errors.Is(g.AddEdge(n1, nf), ErrNodeNotFound))
102 | assert.True(t, errors.Is(g.AddEdge(nf, n1), ErrNodeNotFound))
103 |
104 | assert.True(t, errors.Is(g.AddEdge(n1, nc1), ErrEdgeCrossLevel))
105 |
106 | assert.NoError(t, g.AddEdge(nc1, nc2))
107 |
108 | m, w := nc1.Successors().Get(nc2.name)
109 | assert.Equal(t, nc2, m)
110 | assert.Equal(t, 1, w)
111 | m, w = nc2.Predecessors().Get(nc1.name)
112 | assert.Equal(t, nc1, m)
113 | assert.Equal(t, 1, w)
114 | m, w = n1.Successors().Get(n2.name)
115 | assert.Equal(t, n2, m)
116 | assert.Equal(t, 1, w)
117 | m, w = n2.Predecessors().Get(n1.name)
118 | assert.Equal(t, n1, m)
119 | assert.Equal(t, 1, w)
120 |
121 | assert.NoError(t, g.AddEdge(n1, n2))
122 | m, w = n1.Successors().Get(n2.name)
123 | assert.Equal(t, n2, m)
124 | assert.Equal(t, 2, w)
125 | m, w = n2.Predecessors().Get(n1.name)
126 | assert.Equal(t, n1, m)
127 | assert.Equal(t, 2, w)
128 | })
129 |
130 | t.Run("DeleteEdge", func(t *testing.T) {
131 | nf := newTestNode("non-member", nil)
132 | assert.True(t, errors.Is(g.DeleteEdge(n1, nf), ErrNodeNotFound))
133 | assert.True(t, errors.Is(g.DeleteEdge(nf, n1), ErrNodeNotFound))
134 |
135 | assert.NoError(t, g.DeleteEdge(nc2, nc1))
136 | m, w := nc1.Successors().Get(nc2.name)
137 | assert.Equal(t, nc2, m)
138 | assert.Equal(t, 1, w)
139 | m, w = nc2.Predecessors().Get(nc1.name)
140 | assert.Equal(t, nc1, m)
141 | assert.Equal(t, 1, w)
142 | m, w = n1.Successors().Get(n2.name)
143 | assert.Equal(t, n2, m)
144 | assert.Equal(t, 2, w)
145 | m, w = n2.Predecessors().Get(n1.name)
146 | assert.Equal(t, n1, m)
147 | assert.Equal(t, 2, w)
148 |
149 | assert.NoError(t, g.DeleteEdge(nc1, nc2))
150 | _, w = nc1.Successors().Get(nc2.name)
151 | assert.Equal(t, 0, w)
152 | _, w = nc2.Predecessors().Get(nc1.name)
153 | assert.Equal(t, 0, w)
154 | _, w = n1.Successors().Get(n2.name)
155 | assert.Equal(t, 1, w)
156 | _, w = n2.Predecessors().Get(n1.name)
157 | assert.Equal(t, 1, w)
158 |
159 | assert.NoError(t, g.AddEdge(nc1, nc2))
160 | assert.NoError(t, g.DeleteEdge(n1, n2))
161 | _, w = nc1.Successors().Get(nc2.name)
162 | assert.Equal(t, 0, w)
163 | _, w = nc2.Predecessors().Get(nc1.name)
164 | assert.Equal(t, 0, w)
165 | _, w = n1.Successors().Get(n2.name)
166 | assert.Equal(t, 0, w)
167 | _, w = n2.Predecessors().Get(n1.name)
168 | assert.Equal(t, 0, w)
169 | })
170 |
171 | t.Run("DeleteNode", func(t *testing.T) {
172 | assert.NoError(t, g.AddEdge(n1, n2))
173 |
174 | assert.NoError(t, g.DeleteNode(n2.name))
175 | _, w := n1.Successors().Get(n2.name)
176 | assert.Equal(t, 0, w)
177 | _, err := g.GetNode(nc2.Hash())
178 | assert.True(t, errors.Is(err, ErrNodeNotFound))
179 |
180 | assert.NoError(t, g.AddNode(n2))
181 | assert.NoError(t, g.AddEdge(n1, n2))
182 |
183 | assert.NoError(t, g.DeleteNode(nc1.name))
184 | _, w = n2.Predecessors().Get(n1.name)
185 | assert.Equal(t, 0, w)
186 | _, err = g.GetNode(n1.name)
187 | assert.True(t, errors.Is(err, ErrNodeNotFound))
188 | })
189 | }
190 |
--------------------------------------------------------------------------------
/internal/graph/node.go:
--------------------------------------------------------------------------------
1 | package graph
2 |
3 | import (
4 | "reflect"
5 | "sort"
6 | "strings"
7 | )
8 |
9 | type Node interface {
10 | Name() string
11 | Hash() string
12 | String() string
13 |
14 | Predecessors() *NodeRefs
15 | Successors() *NodeRefs
16 |
17 | Parent() Node
18 | Children() *NodeRefs
19 | }
20 |
21 | func nodeIsNil(n Node) bool {
22 | return n == nil || reflect.ValueOf(n).IsNil()
23 | }
24 |
25 | func nodeDepth(n Node) int {
26 | depth := -1
27 | for !nodeIsNil(n) {
28 | depth++
29 | n = n.Parent()
30 | }
31 | return depth
32 | }
33 |
34 | type NodeRefs struct {
35 | nodeList []Node
36 | nodeMap map[string]Node
37 | weights map[string]int
38 | }
39 |
40 | func NewNodeRefs() NodeRefs {
41 | return NodeRefs{
42 | nodeList: []Node{},
43 | nodeMap: map[string]Node{},
44 | weights: map[string]int{},
45 | }
46 | }
47 |
48 | func (n NodeRefs) String() string {
49 | var ns []string
50 | for _, m := range n.nodeList {
51 | ns = append(ns, m.Name())
52 | }
53 | return strings.Join(ns, ", ")
54 | }
55 |
56 | func (n NodeRefs) Len() int {
57 | return len(n.nodeMap)
58 | }
59 |
60 | func (n *NodeRefs) Add(node Node) {
61 | if nodeIsNil(node) {
62 | return
63 | }
64 |
65 | h := node.Hash()
66 | n.weights[h]++
67 | if _, ok := n.nodeMap[h]; !ok {
68 | n.nodeMap[h] = node
69 | n.nodeList = append(n.nodeList, node)
70 | }
71 | }
72 |
73 | func (n NodeRefs) Get(hash string) (Node, int) {
74 | return n.nodeMap[hash], n.weights[hash]
75 | }
76 |
77 | func (n *NodeRefs) Delete(hash string) {
78 | if n == nil {
79 | return
80 | }
81 |
82 | if _, ok := n.nodeMap[hash]; !ok {
83 | return
84 | }
85 |
86 | n.weights[hash]--
87 | if n.weights[hash] > 0 {
88 | return
89 | }
90 |
91 | delete(n.nodeMap, hash)
92 | delete(n.weights, hash)
93 |
94 | for idx := range n.nodeList {
95 | if n.nodeList[idx].Hash() == hash {
96 | n.nodeList = append(n.nodeList[:idx], n.nodeList[idx+1:]...)
97 | break
98 | }
99 | }
100 | }
101 |
102 | func (n *NodeRefs) Wipe(hash string) {
103 | if n == nil {
104 | return
105 | }
106 |
107 | if _, ok := n.nodeMap[hash]; !ok {
108 | return
109 | }
110 |
111 | delete(n.nodeMap, hash)
112 | delete(n.weights, hash)
113 |
114 | for idx := range n.nodeList {
115 | if n.nodeList[idx].Hash() == hash {
116 | n.nodeList = append(n.nodeList[:idx], n.nodeList[idx+1:]...)
117 | break
118 | }
119 | }
120 | }
121 |
122 | func (n NodeRefs) List() []Node {
123 | sort.Slice(n.nodeList, func(i int, j int) bool { return n.nodeList[i].Name() < n.nodeList[j].Name() })
124 | listCopy := make([]Node, len(n.nodeList))
125 | copy(listCopy, n.nodeList)
126 | return listCopy
127 | }
128 |
--------------------------------------------------------------------------------
/internal/graph/node_test.go:
--------------------------------------------------------------------------------
1 | package graph
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | type testNode struct {
12 | name string
13 | preds NodeRefs
14 | succs NodeRefs
15 | parent *testNode
16 | children NodeRefs
17 | }
18 |
19 | func (n *testNode) Name() string { return n.name }
20 | func (n *testNode) Hash() string { return n.name }
21 | func (n *testNode) String() string {
22 | return fmt.Sprintf("%s, preds: [%s], succs: [%s]", n.name, n.preds, n.succs)
23 | }
24 | func (n *testNode) Predecessors() *NodeRefs { return &n.preds }
25 | func (n *testNode) Successors() *NodeRefs { return &n.succs }
26 | func (n *testNode) Parent() Node { return n.parent }
27 | func (n *testNode) Children() *NodeRefs { return &n.children }
28 |
29 | func newTestNode(name string, parent *testNode) *testNode {
30 | return &testNode{
31 | name: name,
32 | preds: NewNodeRefs(),
33 | succs: NewNodeRefs(),
34 | parent: parent,
35 | children: NewNodeRefs(),
36 | }
37 | }
38 |
39 | func TestEdgesNew(t *testing.T) {
40 | t.Parallel()
41 |
42 | newMap := NewNodeRefs()
43 | assert.NotNil(t, newMap.nodeMap)
44 | assert.NotNil(t, newMap.nodeList)
45 | }
46 |
47 | func TestEdgesAdd(t *testing.T) {
48 | t.Parallel()
49 |
50 | dependencyA := testNode{name: "dependency_a"}
51 |
52 | edges := NewNodeRefs()
53 | edges.Add(&dependencyA)
54 | _, w := edges.Get("dependency_a")
55 | assert.Equal(t, 1, w)
56 | edges.Add(&dependencyA)
57 | _, w = edges.Get("dependency_a")
58 | assert.Equal(t, 2, w)
59 | }
60 |
61 | func TestEdgesDelete(t *testing.T) {
62 | t.Parallel()
63 |
64 | dependencyA := testNode{name: "dependency_a"}
65 |
66 | edges := NewNodeRefs()
67 |
68 | edges.Delete("dependency_a")
69 | assert.Equal(t, 0, edges.Len())
70 |
71 | edges.Add(&dependencyA)
72 | edges.Delete("dependency_a")
73 | assert.Equal(t, 0, edges.Len())
74 | }
75 |
76 | func TestEdgesLen(t *testing.T) {
77 | t.Parallel()
78 |
79 | dependencyA := testNode{name: "dependency_a"}
80 | dependencyB := testNode{name: "dependency_b"}
81 |
82 | edges := NewNodeRefs()
83 | assert.Equal(t, 0, edges.Len())
84 |
85 | edges.Add(&dependencyA)
86 | assert.Equal(t, 1, edges.Len())
87 |
88 | edges.Add(&dependencyA)
89 | assert.Equal(t, 1, edges.Len())
90 |
91 | edges.Add(&dependencyB)
92 | assert.Equal(t, 2, edges.Len())
93 |
94 | edges.Delete("dependency_a")
95 | assert.Equal(t, 2, edges.Len())
96 |
97 | edges.Delete("dependency_b")
98 | assert.Equal(t, 1, edges.Len())
99 | }
100 |
101 | func TestEdgesList(t *testing.T) {
102 | t.Parallel()
103 |
104 | dependencyA := testNode{name: "dependency_a"}
105 | dependencyB := testNode{name: "dependency_b"}
106 |
107 | edges := NewNodeRefs()
108 | edges.Add(&dependencyB)
109 | edges.Add(&dependencyA)
110 |
111 | list := edges.List()
112 | assert.True(t, sort.SliceIsSorted(list, func(i int, j int) bool { return list[i].Name() < list[j].Name() }))
113 | }
114 |
--------------------------------------------------------------------------------
/internal/logger/encoder.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 |
8 | "go.uber.org/zap/buffer"
9 | "go.uber.org/zap/zapcore"
10 | )
11 |
12 | func newEncoder() *goModEncoder {
13 | return &goModEncoder{
14 | Encoder: zapcore.NewConsoleEncoder(fieldEncoderConfig),
15 | EncoderConfig: externalEncoderConfig,
16 | }
17 | }
18 |
19 | type goModEncoder struct {
20 | zapcore.Encoder
21 | zapcore.EncoderConfig
22 |
23 | indent int
24 | }
25 |
26 | func (c goModEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
27 | line := pool.Get()
28 |
29 | arr := &sliceArrayEncoder{}
30 | c.EncodeTime(ent.Time, arr)
31 | c.EncodeLevel(ent.Level, arr)
32 | c.EncodeName(ent.LoggerName, arr)
33 | if c.indent > 0 {
34 | arr.AppendString(strings.Repeat("-", c.indent))
35 | }
36 | if ent.Caller.Defined && c.EncodeCaller != nil {
37 | c.EncodeCaller(ent.Caller, arr)
38 | }
39 | arr.AppendString(ent.Message)
40 |
41 | for i := range arr.elems {
42 | if i > 0 {
43 | line.AppendByte(' ')
44 | }
45 | fmt.Fprint(line, arr.elems[i])
46 | }
47 |
48 | b, err := c.Encoder.EncodeEntry(zapcore.Entry{}, fields)
49 | switch {
50 | case err != nil:
51 | return nil, err
52 | case b.Len() > 0:
53 | line.AppendByte(' ')
54 | if _, err = line.Write(b.Bytes()); err != nil {
55 | return nil, err
56 | }
57 | default:
58 | line.AppendString("\n")
59 | }
60 |
61 | return line, nil
62 | }
63 |
64 | var (
65 | pool = buffer.NewPool()
66 |
67 | externalEncoderConfig = zapcore.EncoderConfig{
68 | LevelKey: "level",
69 | TimeKey: "time",
70 | MessageKey: "msg",
71 | CallerKey: "caller",
72 | EncodeLevel: zapcore.LowercaseColorLevelEncoder,
73 | EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("15:04:05")) },
74 | EncodeDuration: zapcore.MillisDurationEncoder,
75 | EncodeCaller: zapcore.FullCallerEncoder,
76 | EncodeName: zapcore.FullNameEncoder,
77 | }
78 | fieldEncoderConfig = zapcore.EncoderConfig{
79 | // We omit any of the keys so that we don't print any of the already previously printed fields.
80 | EncodeLevel: zapcore.LowercaseColorLevelEncoder,
81 | EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("15:04:05")) },
82 | EncodeDuration: zapcore.MillisDurationEncoder,
83 | EncodeCaller: zapcore.FullCallerEncoder,
84 | }
85 | )
86 |
87 | type sliceArrayEncoder struct {
88 | elems []interface{}
89 | }
90 |
91 | func (s *sliceArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error {
92 | enc := &sliceArrayEncoder{}
93 | err := v.MarshalLogArray(enc)
94 | s.elems = append(s.elems, enc.elems)
95 | return err
96 | }
97 |
98 | func (s *sliceArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error {
99 | m := zapcore.NewMapObjectEncoder()
100 | err := v.MarshalLogObject(m)
101 | s.elems = append(s.elems, m.Fields)
102 | return err
103 | }
104 |
105 | func (s *sliceArrayEncoder) AppendReflected(v interface{}) error {
106 | s.elems = append(s.elems, v)
107 | return nil
108 | }
109 |
110 | func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) }
111 | func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, string(v)) }
112 | func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) }
113 | func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) }
114 | func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) }
115 | func (s *sliceArrayEncoder) AppendFloat64(v float64) { s.elems = append(s.elems, v) }
116 | func (s *sliceArrayEncoder) AppendFloat32(v float32) { s.elems = append(s.elems, v) }
117 | func (s *sliceArrayEncoder) AppendInt(v int) { s.elems = append(s.elems, v) }
118 | func (s *sliceArrayEncoder) AppendInt64(v int64) { s.elems = append(s.elems, v) }
119 | func (s *sliceArrayEncoder) AppendInt32(v int32) { s.elems = append(s.elems, v) }
120 | func (s *sliceArrayEncoder) AppendInt16(v int16) { s.elems = append(s.elems, v) }
121 | func (s *sliceArrayEncoder) AppendInt8(v int8) { s.elems = append(s.elems, v) }
122 | func (s *sliceArrayEncoder) AppendString(v string) { s.elems = append(s.elems, v) }
123 | func (s *sliceArrayEncoder) AppendTime(v time.Time) { s.elems = append(s.elems, v) }
124 | func (s *sliceArrayEncoder) AppendUint(v uint) { s.elems = append(s.elems, v) }
125 | func (s *sliceArrayEncoder) AppendUint64(v uint64) { s.elems = append(s.elems, v) }
126 | func (s *sliceArrayEncoder) AppendUint32(v uint32) { s.elems = append(s.elems, v) }
127 | func (s *sliceArrayEncoder) AppendUint16(v uint16) { s.elems = append(s.elems, v) }
128 | func (s *sliceArrayEncoder) AppendUint8(v uint8) { s.elems = append(s.elems, v) }
129 | func (s *sliceArrayEncoder) AppendUintptr(v uintptr) { s.elems = append(s.elems, v) }
130 |
--------------------------------------------------------------------------------
/internal/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "go.uber.org/zap"
5 | "go.uber.org/zap/zapcore"
6 | )
7 |
8 | type Domain uint8
9 |
10 | const (
11 | UnknownDomain Domain = iota
12 | AllDomain
13 | InitDomain
14 | GraphDomain
15 | ModuleInfoDomain
16 | PackageInfoDomain
17 | ModuleDependencyDomain
18 | ParserDomain
19 | QueryDomain
20 | PrinterDomain
21 | )
22 |
23 | func domainFromString(domain string) Domain {
24 | return map[string]Domain{
25 | "all": AllDomain,
26 | "init": InitDomain,
27 | "graph": GraphDomain,
28 | "modinfo": ModuleInfoDomain,
29 | "pkginfo": PackageInfoDomain,
30 | "moddeps": ModuleDependencyDomain,
31 | "parser": ParserDomain,
32 | "query": QueryDomain,
33 | "printer": PrinterDomain,
34 | }[domain]
35 | }
36 |
37 | func stringFromDomain(domain Domain) string {
38 | return map[Domain]string{
39 | AllDomain: "all",
40 | InitDomain: "init",
41 | GraphDomain: "graph",
42 | ModuleInfoDomain: "modinfo",
43 | PackageInfoDomain: "pkginfo",
44 | ModuleDependencyDomain: "moddeps",
45 | ParserDomain: "parser",
46 | QueryDomain: "query",
47 | PrinterDomain: "printer",
48 | }[domain]
49 | }
50 |
51 | type Builder struct {
52 | log *zap.Logger
53 | enc *goModEncoder
54 | defaultLevel zapcore.Level
55 | domainLevels map[Domain]zapcore.Level
56 | cache map[Domain]*Logger
57 | }
58 |
59 | func NewBuilder(out zapcore.WriteSyncer) *Builder {
60 | enc := newEncoder()
61 | return &Builder{
62 | log: zap.New(zapcore.NewCore(enc, out, zapcore.DebugLevel)),
63 | enc: enc,
64 | domainLevels: map[Domain]zapcore.Level{},
65 | cache: map[Domain]*Logger{},
66 | }
67 | }
68 |
69 | func (b *Builder) SetDomainLevel(domain string, level zapcore.Level) {
70 | d := domainFromString(domain)
71 | switch d {
72 | case UnknownDomain:
73 | b.log.Warn("Unrecognised logger domain.")
74 | case AllDomain:
75 | b.defaultLevel = level
76 | default:
77 | b.domainLevels[d] = level
78 | }
79 | }
80 |
81 | func (b *Builder) Log() *Logger {
82 | return b.logger(AllDomain)
83 | }
84 |
85 | func (b *Builder) Domain(domain Domain) *Logger {
86 | return b.logger(domain)
87 | }
88 |
89 | func (b *Builder) logger(domain Domain) *Logger {
90 | if _, ok := b.cache[domain]; !ok {
91 | targetLevel := b.defaultLevel
92 | if lvl, ok := b.domainLevels[domain]; ok {
93 | targetLevel = lvl
94 | }
95 | b.cache[domain] = &Logger{
96 | Logger: b.log.Named(stringFromDomain(domain)).WithOptions(zap.IncreaseLevel(targetLevel)),
97 | enc: b.enc,
98 | }
99 | }
100 | return b.cache[domain]
101 | }
102 |
103 | type Logger struct {
104 | *zap.Logger
105 | enc *goModEncoder
106 | }
107 |
108 | func (l *Logger) AddIndent() {
109 | l.enc.indent++
110 | }
111 |
112 | func (l *Logger) RemoveIndent() {
113 | if l.enc.indent > 0 {
114 | l.enc.indent--
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/internal/modules/modules.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "time"
9 |
10 | "go.uber.org/zap"
11 |
12 | "github.com/Helcaraxan/gomod/internal/logger"
13 | "github.com/Helcaraxan/gomod/internal/util"
14 | )
15 |
16 | // ModuleInfo represents the data returned by 'go list -m --json' for a Go module. It's content is
17 | // extracted directly from the Go documentation.
18 | type ModuleInfo struct {
19 | Main bool // is this the main module?
20 | Indirect bool // is it an indirect dependency?
21 | Path string // module path
22 | Replace *ModuleInfo // replaced by this module
23 | Version string // module version
24 | Time *time.Time // time version was created
25 | Dir string // location of the module's source
26 | Update *ModuleInfo // available update, if any (with -u)
27 | GoMod string // the path to this module's go.mod file
28 | GoVersion string // the Go version associated with the module
29 | Error *ModuleError // error loading module
30 | }
31 |
32 | // ModuleError represents the data that is returned whenever Go tooling was unable to load a given
33 | // module's information.
34 | type ModuleError struct {
35 | Err string // the error itself
36 | }
37 |
38 | // Retrieve the Module information for all dependencies of the Go module found at the specified path.
39 | func GetDependencies(log *logger.Logger, moduleDir string) (*ModuleInfo, map[string]*ModuleInfo, error) {
40 | return retrieveModuleInformation(log, moduleDir, "all")
41 | }
42 |
43 | // Retrieve the Module information for all dependencies of the Go module found at the specified
44 | // path, including any potentially available updates. This requires internet connectivity in order
45 | // to return the results. Lack of connectivity should result in an error being returned but this is
46 | // not a hard guarantee.
47 | func GetDependenciesWithUpdates(log *logger.Logger, moduleDir string) (*ModuleInfo, map[string]*ModuleInfo, error) {
48 | return retrieveModuleInformation(log, moduleDir, "all", "-versions", "-u")
49 | }
50 |
51 | // Retrieve the Module information for the specified target module which must be a dependency of the
52 | // Go module found at the specified path.
53 | func GetModule(log *logger.Logger, moduleDir string, targetModule string) (*ModuleInfo, error) {
54 | module, _, err := retrieveModuleInformation(log, moduleDir, targetModule)
55 | return module, err
56 | }
57 |
58 | // Retrieve the Module information for the specified target module which must be a dependency of the
59 | // Go module found at the specified path, including any potentially available updates. This requires
60 | // internet connectivity in order to return the results. Lack of connectivity should result in an
61 | // error being returned but this is not a hard guarantee.
62 | func GetModuleWithUpdate(log *logger.Logger, moduleDir string, targetModule string) (*ModuleInfo, error) {
63 | module, _, err := retrieveModuleInformation(log, moduleDir, targetModule, "-versions", "-u")
64 | return module, err
65 | }
66 |
67 | func retrieveModuleInformation(
68 | log *logger.Logger,
69 | moduleDir string,
70 | targetModule string,
71 | extraGoListArgs ...string,
72 | ) (*ModuleInfo, map[string]*ModuleInfo, error) {
73 | log.Debug("Ensuring module information is available locally by running 'go mod download'.")
74 | _, _, err := util.RunCommand(log, moduleDir, "go", "mod", "download")
75 | if err != nil {
76 | log.Error("Failed to run 'go mod download'.", zap.Error(err))
77 | return nil, nil, err
78 | }
79 |
80 | log.Debug("Retrieving module information via 'go list'")
81 | goListArgs := append([]string{"list", "-json", "-m", "-mod=mod"}, extraGoListArgs...)
82 | if targetModule == "" {
83 | targetModule = "all"
84 | }
85 | goListArgs = append(goListArgs, targetModule)
86 |
87 | raw, _, err := util.RunCommand(log, moduleDir, "go", goListArgs...)
88 | if err != nil {
89 | log.Error("Failed to list modules in dependency graph via 'go list'.", zap.Error(err))
90 | return nil, nil, err
91 | }
92 | raw = bytes.ReplaceAll(bytes.TrimSpace(raw), []byte("\n}\n"), []byte("\n},\n"))
93 | raw = append([]byte("[\n"), raw...)
94 | raw = append(raw, []byte("\n]")...)
95 |
96 | var moduleList []*ModuleInfo
97 | if err = json.Unmarshal(raw, &moduleList); err != nil {
98 | return nil, nil, fmt.Errorf("Unable to retrieve information from 'go list': %v", err)
99 | }
100 |
101 | var main *ModuleInfo
102 | modules := map[string]*ModuleInfo{}
103 | for _, module := range moduleList {
104 | if module.Error != nil {
105 | log.Warn("Unable to retrieve information for module", zap.String("module", module.Path), zap.String("error", module.Error.Err))
106 | continue
107 | }
108 |
109 | if module.Main {
110 | main = module
111 | }
112 | modules[module.Path] = module
113 | if module.Replace != nil {
114 | modules[module.Replace.Path] = module
115 | }
116 | }
117 | if len(modules) == 0 {
118 | return nil, nil, errors.New("unable to load any module information")
119 | }
120 | return main, modules, nil
121 | }
122 |
--------------------------------------------------------------------------------
/internal/modules/modules_test.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/Helcaraxan/gomod/internal/testutil"
14 | )
15 |
16 | type testcase struct {
17 | DriverError bool `yaml:"driver_error"`
18 | ListModOutput map[string]string `yaml:"go_list_mod_output"`
19 | ListPkgOutput map[string]string `yaml:"go_list_pkg_output"`
20 |
21 | ExpectedError bool `yaml:"error"`
22 | ExpectedMain *ModuleInfo `yaml:"main"`
23 | ExpectedModules map[string]*ModuleInfo `yaml:"modules"`
24 | }
25 |
26 | func (c *testcase) GoDriverError() bool { return c.DriverError }
27 | func (c *testcase) GoListModOutput() map[string]string { return c.ListModOutput }
28 | func (c *testcase) GoListPkgOutput() map[string]string { return c.ListPkgOutput }
29 | func (c *testcase) GoGraphOutput() string { return "" }
30 |
31 | func TestModuleInformationRetrieval(t *testing.T) {
32 | cwd, err := os.Getwd()
33 | require.NoError(t, err)
34 |
35 | // Prepend the testdata directory to the path so we use the fake "go" script.
36 | err = os.Setenv("PATH", filepath.Join(cwd, "..", "internal", "testutil")+":"+os.Getenv("PATH"))
37 | require.NoError(t, err)
38 |
39 | files, err := ioutil.ReadDir(filepath.Join(cwd, "testdata"))
40 | require.NoError(t, err)
41 |
42 | for idx := range files {
43 | file := files[idx]
44 | if file.IsDir() || filepath.Ext(file.Name()) != ".yaml" {
45 | continue
46 | }
47 |
48 | testname := strings.TrimSuffix(file.Name(), ".yaml")
49 | t.Run(testname, func(t *testing.T) {
50 | testDefinition := &testcase{}
51 | testDir := testutil.SetupTestModule(t, filepath.Join(cwd, "testdata", file.Name()), testDefinition)
52 |
53 | log := testutil.TestLogger(t)
54 |
55 | main, modules, testErr := GetDependencies(log.Log(), testDir)
56 | if testDefinition.ExpectedError {
57 | assert.Error(t, testErr)
58 | } else {
59 | require.NoError(t, testErr)
60 | assert.Equal(t, testDefinition.ExpectedMain, main)
61 | assert.Equal(t, testDefinition.ExpectedModules, modules)
62 | }
63 |
64 | main, modules, testErr = GetDependenciesWithUpdates(log.Log(), testDir)
65 | if testDefinition.ExpectedError {
66 | assert.Error(t, testErr)
67 | } else {
68 | require.NoError(t, testErr)
69 | assert.Equal(t, testDefinition.ExpectedMain, main)
70 | assert.Equal(t, testDefinition.ExpectedModules, modules)
71 | }
72 |
73 | if !testDefinition.ExpectedError {
74 | main, testErr = GetModule(log.Log(), testDir, testDefinition.ExpectedMain.Path)
75 | require.NoError(t, testErr)
76 | assert.Equal(t, testDefinition.ExpectedMain, main)
77 |
78 | main, testErr = GetModuleWithUpdate(log.Log(), testDir, testDefinition.ExpectedMain.Path)
79 | require.NoError(t, testErr)
80 | assert.Equal(t, testDefinition.ExpectedMain, main)
81 | }
82 | })
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/internal/modules/packages.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | // PackageInfo represents the data returned by 'go list --json' for a Go package. It's content is
4 | // extracted directly from the Go documentation.
5 | type PackageInfo struct {
6 | Dir string // directory containing package sources
7 | ImportPath string // import path of package in dir
8 | ImportComment string // path in import comment on package statement
9 | Name string // package name
10 | Doc string // package documentation string
11 | Target string // install path
12 | Shlib string // the shared library that contains this package (only set when -linkshared)
13 | Goroot bool // is this package in the Go root?
14 | Standard bool // is this package part of the standard Go library?
15 | Stale bool // would 'go install' do anything for this package?
16 | StaleReason string // explanation for Stale==true
17 | Root string // Go root or Go path dir containing this package
18 | ConflictDir string // this directory shadows Dir in $GOPATH
19 | BinaryOnly bool // binary-only package (no longer supported)
20 | ForTest string // package is only for use in named test
21 | Export string // file containing export data (when using -export)
22 | Module *ModuleInfo // info about package's containing module, if any (can be nil)
23 | Match []string // command-line patterns matching this package
24 | DepOnly bool // package is only a dependency, not explicitly listed
25 |
26 | // Source files
27 | GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
28 | CgoFiles []string // .go source files that import "C"
29 | CompiledGoFiles []string // .go files presented to compiler (when using -compiled)
30 | IgnoredGoFiles []string // .go source files ignored due to build constraints
31 | CFiles []string // .c source files
32 | CXXFiles []string // .cc, .cxx and .cpp source files
33 | MFiles []string // .m source files
34 | HFiles []string // .h, .hh, .hpp and .hxx source files
35 | FFiles []string // .f, .F, .for and .f90 Fortran source files
36 | SFiles []string // .s source files
37 | SwigFiles []string // .swig files
38 | SwigCXXFiles []string // .swigcxx files
39 | SysoFiles []string // .syso object files to add to archive
40 | TestGoFiles []string // _test.go files in package
41 | XTestGoFiles []string // _test.go files outside package
42 |
43 | // Cgo directives
44 | CgoCFLAGS []string // cgo: flags for C compiler
45 | CgoCPPFLAGS []string // cgo: flags for C preprocessor
46 | CgoCXXFLAGS []string // cgo: flags for C++ compiler
47 | CgoFFLAGS []string // cgo: flags for Fortran compiler
48 | CgoLDFLAGS []string // cgo: flags for linker
49 | CgoPkgConfig []string // cgo: pkg-config names
50 |
51 | // Dependency information
52 | Imports []string // import paths used by this package
53 | ImportMap map[string]string // map from source import to ImportPath (identity entries omitted)
54 | Deps []string // all (recursively) imported dependencies
55 | TestImports []string // imports from TestGoFiles
56 | XTestImports []string // imports from XTestGoFiles
57 |
58 | // Error information
59 | Incomplete bool // this package or a dependency has an error
60 | Error *PackageError // error loading package
61 | DepsErrors []*PackageError // errors loading dependencies
62 | }
63 |
64 | type PackageError struct {
65 | ImportStack []string // shortest path from package named on command line to this one
66 | Pos string // position of error (if present, file:line:col)
67 | Err string // the error itself
68 | }
69 |
--------------------------------------------------------------------------------
/internal/modules/testdata/GoListError.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | driver_error: true
3 | error: true
4 |
--------------------------------------------------------------------------------
/internal/modules/testdata/InvalidListOutput.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | github.com/Helcaraxan/gomod: |
4 | This is no JSON
5 | error: true
6 |
--------------------------------------------------------------------------------
/internal/modules/testdata/LoadError.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | test-module: |
4 | {
5 | "Error": {
6 | "Err": "could not load module"
7 | }
8 | }
9 | error: true
10 |
--------------------------------------------------------------------------------
/internal/modules/testdata/LoadErrorNotFatal.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | github.com/Helcaraxan/gomod: |
4 | {
5 | "Path": "github.com/Helcaraxan/gomod",
6 | "Main": true,
7 | "Dir": "/Users/duco/go/src/github.com/Helcaraxan/gomod",
8 | "GoMod": "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
9 | }
10 | {
11 | "Error": {
12 | "Err": "could not load module"
13 | }
14 | }
15 | main:
16 | path: "github.com/Helcaraxan/gomod"
17 | main: true
18 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
19 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
20 | modules:
21 | "github.com/Helcaraxan/gomod":
22 | path: "github.com/Helcaraxan/gomod"
23 | main: true
24 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
25 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
26 |
--------------------------------------------------------------------------------
/internal/modules/testdata/NoDependencies.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | github.com/Helcaraxan/gomod: |
4 | {
5 | "Path": "github.com/Helcaraxan/gomod",
6 | "Main": true,
7 | "Dir": "/Users/duco/go/src/github.com/Helcaraxan/gomod",
8 | "GoMod": "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
9 | }
10 | main:
11 | path: "github.com/Helcaraxan/gomod"
12 | main: true
13 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
14 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
15 | modules:
16 | "github.com/Helcaraxan/gomod":
17 | path: "github.com/Helcaraxan/gomod"
18 | main: true
19 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
20 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
21 |
--------------------------------------------------------------------------------
/internal/modules/testdata/NoModule.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | error: true
4 |
--------------------------------------------------------------------------------
/internal/modules/testdata/ReplacedDependency.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | go_list_mod_output:
3 | github.com/Helcaraxan/gomod: |
4 | {
5 | "Path": "github.com/Helcaraxan/gomod",
6 | "Main": true,
7 | "Dir": "/Users/duco/go/src/github.com/Helcaraxan/gomod",
8 | "GoMod": "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
9 | }
10 | github.com/foo/bar: |
11 | {
12 | "Path": "github.com/foo/bar",
13 | "Replace": {
14 | "Path": "../test_bar",
15 | "GoMod": "/foo/test_bar/go.mod"
16 | },
17 | "Dir": "/foo/bar",
18 | "GoMod": "/foo/bar/go.mod"
19 | }
20 | main:
21 | path: "github.com/Helcaraxan/gomod"
22 | main: true
23 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
24 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
25 | modules:
26 | "github.com/Helcaraxan/gomod":
27 | path: "github.com/Helcaraxan/gomod"
28 | main: true
29 | dir: "/Users/duco/go/src/github.com/Helcaraxan/gomod"
30 | gomod: "/Users/duco/go/src/github.com/Helcaraxan/gomod/go.mod"
31 | "github.com/foo/bar":
32 | path: "github.com/foo/bar"
33 | replace:
34 | path: "../test_bar"
35 | gomod: "/foo/test_bar/go.mod"
36 | dir: "/foo/bar"
37 | gomod: "/foo/bar/go.mod"
38 | "../test_bar":
39 | path: "github.com/foo/bar"
40 | replace:
41 | path: "../test_bar"
42 | gomod: "/foo/test_bar/go.mod"
43 | dir: "/foo/bar"
44 | gomod: "/foo/bar/go.mod"
45 |
--------------------------------------------------------------------------------
/internal/parsers/style.go:
--------------------------------------------------------------------------------
1 | package parsers
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "go.uber.org/zap"
8 |
9 | "github.com/Helcaraxan/gomod/internal/logger"
10 | "github.com/Helcaraxan/gomod/internal/printer"
11 | )
12 |
13 | func ParseStyleConfiguration(log *logger.Logger, config string) (*printer.StyleOptions, error) {
14 | styleOptions := &printer.StyleOptions{}
15 | for _, setting := range strings.Split(config, ",") {
16 | if setting == "" {
17 | continue
18 | }
19 |
20 | configKey := setting
21 | var configValue string
22 | if valueIdx := strings.Index(setting, "="); valueIdx >= 0 {
23 | configKey = setting[:valueIdx]
24 | configValue = setting[valueIdx+1:]
25 | }
26 | configKey = strings.ToLower(strings.TrimSpace(configKey))
27 | configValue = strings.ToLower(strings.TrimSpace(configValue))
28 |
29 | var err error
30 | switch configKey {
31 | case "scale_nodes":
32 | err = parseStyleScaleNodes(log, styleOptions, configValue)
33 | case "cluster":
34 | err = parseStyleCluster(log, styleOptions, configValue)
35 | default:
36 | log.Error("Skipping unknown style option.", zap.String("option", configKey))
37 | err = errors.New("invalid config")
38 | }
39 | if err != nil {
40 | return nil, err
41 | }
42 | }
43 | return styleOptions, nil
44 | }
45 |
46 | func parseStyleScaleNodes(log *logger.Logger, styleOptions *printer.StyleOptions, raw string) error {
47 | switch strings.ToLower(raw) {
48 | case "", "true", "on", "yes":
49 | styleOptions.ScaleNodes = true
50 | case "false", "off", "no":
51 | styleOptions.ScaleNodes = false
52 | default:
53 | log.Error("Could not set 'scale_nodes' style. Accepted values are 'true' and 'false'.", zap.String("value", raw))
54 | return errors.New("invalid 'scale_nodes' value")
55 | }
56 | return nil
57 | }
58 |
59 | func parseStyleCluster(log *logger.Logger, styleOptions *printer.StyleOptions, raw string) error {
60 | switch strings.ToLower(raw) {
61 | case "off", "false", "no":
62 | styleOptions.Cluster = printer.Off
63 | case "", "shared", "on", "true", "yes":
64 | styleOptions.Cluster = printer.Shared
65 | case "full":
66 | styleOptions.Cluster = printer.Full
67 | default:
68 | log.Error("Could not set 'cluster' style. Accepted values are 'off', 'shared' and 'full'.", zap.String("value", raw))
69 | return errors.New("invalid 'cluster' value")
70 | }
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/internal/parsers/style_test.go:
--------------------------------------------------------------------------------
1 | package parsers
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/require"
8 |
9 | "github.com/Helcaraxan/gomod/internal/printer"
10 | "github.com/Helcaraxan/gomod/internal/testutil"
11 | )
12 |
13 | func TestVisualConfig(t *testing.T) {
14 | testcases := map[string]struct {
15 | optionValue string
16 | expectedConfig *printer.StyleOptions
17 | expectedError bool
18 | }{
19 | "Empty": {
20 | optionValue: "",
21 | expectedConfig: &printer.StyleOptions{},
22 | },
23 | "ScaleNodesFalse": {
24 | optionValue: "scale_nodes=false",
25 | expectedConfig: &printer.StyleOptions{ScaleNodes: false},
26 | },
27 | "ScaleNodesNo": {
28 | optionValue: "scale_nodes=no",
29 | expectedConfig: &printer.StyleOptions{ScaleNodes: false},
30 | },
31 | "ScaleNodesOff": {
32 | optionValue: "scale_nodes=off",
33 | expectedConfig: &printer.StyleOptions{ScaleNodes: false},
34 | },
35 | "ScaleNodesEmpty": {
36 | optionValue: "scale_nodes",
37 | expectedConfig: &printer.StyleOptions{ScaleNodes: true},
38 | },
39 | "ScaleNodesTrue": {
40 | optionValue: "scale_nodes=true",
41 | expectedConfig: &printer.StyleOptions{ScaleNodes: true},
42 | },
43 | "ScaleNodesYes": {
44 | optionValue: "scale_nodes=yes",
45 | expectedConfig: &printer.StyleOptions{ScaleNodes: true},
46 | },
47 | "ScaleNodesOn": {
48 | optionValue: "scale_nodes=on",
49 | expectedConfig: &printer.StyleOptions{ScaleNodes: true},
50 | },
51 | "ClusterFalse": {
52 | optionValue: "cluster=false",
53 | expectedConfig: &printer.StyleOptions{Cluster: printer.Off},
54 | },
55 | "ClusterNo": {
56 | optionValue: "cluster=no",
57 | expectedConfig: &printer.StyleOptions{Cluster: printer.Off},
58 | },
59 | "ClusterOff": {
60 | optionValue: "cluster=off",
61 | expectedConfig: &printer.StyleOptions{Cluster: printer.Off},
62 | },
63 | "ClusterEmpty": {
64 | optionValue: "cluster",
65 | expectedConfig: &printer.StyleOptions{Cluster: printer.Shared},
66 | },
67 | "ClusterShared": {
68 | optionValue: "cluster=shared",
69 | expectedConfig: &printer.StyleOptions{Cluster: printer.Shared},
70 | },
71 | "ClusterTrue": {
72 | optionValue: "cluster=true",
73 | expectedConfig: &printer.StyleOptions{Cluster: printer.Shared},
74 | },
75 | "ClusterOn": {
76 | optionValue: "cluster=on",
77 | expectedConfig: &printer.StyleOptions{Cluster: printer.Shared},
78 | },
79 | "ClusterYes": {
80 | optionValue: "cluster=yes",
81 | expectedConfig: &printer.StyleOptions{Cluster: printer.Shared},
82 | },
83 | "ClusterFull": {
84 | optionValue: "cluster=full",
85 | expectedConfig: &printer.StyleOptions{Cluster: printer.Full},
86 | },
87 | "AllConfigsSimple": {
88 | optionValue: "cluster=true,scale_nodes=true",
89 | expectedConfig: &printer.StyleOptions{
90 | Cluster: printer.Shared,
91 | ScaleNodes: true,
92 | },
93 | },
94 | "AllConfigsComplex": {
95 | optionValue: "cluster=True , scale_nodes = tRuE",
96 | expectedConfig: &printer.StyleOptions{
97 | Cluster: printer.Shared,
98 | ScaleNodes: true,
99 | },
100 | },
101 | "UnknownConfig": {
102 | optionValue: "foo",
103 | expectedError: true,
104 | },
105 | "UnknownScaleNodeValue": {
106 | optionValue: "scale_nodes=foo",
107 | expectedError: true,
108 | },
109 | "UnknownClusterValue": {
110 | optionValue: "cluster=foo",
111 | expectedError: true,
112 | },
113 | }
114 |
115 | for name := range testcases {
116 | testcase := testcases[name]
117 | t.Run(name, func(t *testing.T) {
118 | log := testutil.TestLogger(t)
119 | config, err := ParseStyleConfiguration(log.Log(), testcase.optionValue)
120 | if testcase.expectedError {
121 | assert.Error(t, err)
122 | } else {
123 | require.NoError(t, err)
124 | assert.Equal(t, testcase.expectedConfig, config)
125 | }
126 | })
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/internal/printer/clustering.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | import (
4 | "math"
5 | "sort"
6 | "strings"
7 | "unicode"
8 |
9 | "github.com/Helcaraxan/gomod/internal/graph"
10 | )
11 |
12 | func computeGraphClusters(g *graph.HierarchicalDigraph, config *PrintConfig) *graphClusters {
13 | graphClusters := &graphClusters{
14 | graph: g,
15 | level: config.Granularity,
16 | clusterMap: map[string]*graphCluster{},
17 | cachedDepthMaps: map[string]map[string]int{},
18 | }
19 |
20 | hashToCluster := map[string]*graphCluster{}
21 | for _, node := range g.GetLevel(int(config.Granularity)).List() {
22 | clusterHash := computeClusterHash(config, node)
23 | cluster := hashToCluster[clusterHash]
24 | if cluster == nil {
25 | cluster = newGraphCluster(clusterHash)
26 | hashToCluster[clusterHash] = cluster
27 | graphClusters.clusterList = append(graphClusters.clusterList, cluster)
28 | }
29 | cluster.members = append(cluster.members, node)
30 | graphClusters.clusterMap[node.Hash()] = cluster
31 | }
32 |
33 | // Ensure determinism by sorting the nodes in each cluster. The order that is used puts nodes
34 | // with no dependencies first and those with at least one last, the rest of the ordering is done
35 | // by alphabetical order.
36 | for hash := range hashToCluster {
37 | cluster := hashToCluster[hash]
38 | sort.Slice(cluster.members, func(i int, j int) bool {
39 | hasDepsI := cluster.members[i].Successors().Len() > 0
40 | hasDepsJ := cluster.members[j].Successors().Len() > 0
41 | if (hasDepsI && !hasDepsJ) || (!hasDepsI && hasDepsJ) {
42 | return hasDepsJ
43 | }
44 | return cluster.members[i].Name() < cluster.members[j].Name()
45 | })
46 | }
47 | sort.Slice(graphClusters.clusterList, func(i int, j int) bool {
48 | return graphClusters.clusterList[i].hash < graphClusters.clusterList[j].hash
49 | })
50 | return graphClusters
51 | }
52 |
53 | func computeClusterHash(config *PrintConfig, node graph.Node) string {
54 | var hashElements []string
55 | for _, pred := range node.Predecessors().List() {
56 | hashElements = append(hashElements, nodeNameToHash(pred.Name()))
57 | }
58 | sort.Strings(hashElements)
59 | hash := strings.Join(hashElements, "_")
60 |
61 | // Depending on the configuration we need to generate more or less unique cluster names.
62 | if config.Style == nil || config.Style.Cluster == Off || (config.Style.Cluster == Shared && node.Predecessors().Len() > 1) {
63 | hash = node.Name() + "_from_" + hash
64 | }
65 | return hash
66 | }
67 |
68 | type graphClusters struct {
69 | graph *graph.HierarchicalDigraph
70 | level Level
71 |
72 | clusterMap map[string]*graphCluster
73 | clusterList []*graphCluster
74 |
75 | cachedDepthMaps map[string]map[string]int
76 | }
77 |
78 | func (c *graphClusters) clusterDepthMap(nodeHash string) map[string]int {
79 | if m, ok := c.cachedDepthMaps[nodeHash]; ok {
80 | return m
81 | }
82 |
83 | depthMap := map[string]int{}
84 | levelNodes := c.graph.GetLevel(int(c.level))
85 |
86 | startNode, _ := levelNodes.Get(nodeHash)
87 | workStack := []graph.Node{startNode}
88 | workMap := map[string]int{nodeHash: 0}
89 | pathLength := 0
90 | for len(workStack) > 0 {
91 | pathLength++
92 | curr := workStack[len(workStack)-1]
93 | if counter, ok := workMap[curr.Hash()]; ok && counter > 0 { // Reached leaf of the DFS or cycle detected.
94 | workStack = workStack[:len(workStack)-1]
95 | pathLength--
96 | if counter == pathLength-1 { // Reached leaf of the DFS.
97 | delete(workMap, curr.Hash())
98 | }
99 | continue
100 | }
101 | workMap[curr.Hash()] = pathLength
102 |
103 | currentDepth := depthMap[curr.Hash()]
104 | baseEdgeLength := c.clusterMap[curr.Hash()].getHeight()
105 | for _, pred := range curr.Predecessors().List() {
106 | predNode, _ := levelNodes.Get(pred.Hash())
107 | edgeLength := baseEdgeLength + c.clusterMap[curr.Hash()].getDepCount()/20 // Give bonus space for larger numbers of edges.
108 | if depthMap[pred.Hash()] >= currentDepth+edgeLength {
109 | continue
110 | }
111 | depthMap[pred.Hash()] = currentDepth + edgeLength
112 | if _, ok := workMap[pred.Hash()]; !ok { // Only allow one instance of a node in the queue.
113 | workMap[pred.Hash()] = 0
114 | workStack = append(workStack, predNode)
115 | }
116 | }
117 | }
118 | c.cachedDepthMaps[nodeHash] = depthMap
119 |
120 | return depthMap
121 | }
122 |
123 | type graphCluster struct {
124 | id int
125 | hash string
126 | members []graph.Node
127 |
128 | cachedDepCount int
129 | cachedWidth int
130 | }
131 |
132 | var clusterIDCounter int
133 |
134 | func newGraphCluster(hash string) *graphCluster {
135 | clusterIDCounter++
136 | return &graphCluster{
137 | id: clusterIDCounter,
138 | hash: hash,
139 | cachedDepCount: -1,
140 | cachedWidth: -1,
141 | }
142 | }
143 |
144 | var alphaNumericalRange = []*unicode.RangeTable{unicode.Letter, unicode.Number}
145 |
146 | func (c *graphCluster) name() string {
147 | if len(c.members) > 1 {
148 | return "cluster_" + c.hash
149 | }
150 | return c.hash
151 | }
152 |
153 | func (c *graphCluster) getRepresentative() string {
154 | if len(c.members) == 0 {
155 | return ""
156 | }
157 | return c.members[c.getWidth()/2].Name()
158 | }
159 |
160 | func (c *graphCluster) getDepCount() int {
161 | if c.cachedDepCount >= 0 {
162 | return c.cachedDepCount
163 | }
164 |
165 | var depCount int
166 | for idx := len(c.members) - 1; idx >= 0; idx-- {
167 | if c.members[idx].Successors().Len() == 0 {
168 | break
169 | }
170 | depCount += c.members[idx].Successors().Len()
171 | }
172 | c.cachedDepCount = depCount
173 | return depCount
174 | }
175 |
176 | func (c *graphCluster) getHeight() int {
177 | width := c.getWidth()
178 | heigth := len(c.members) / width
179 | if len(c.members)%width != 0 {
180 | heigth++
181 | }
182 | if heigth > 1 {
183 | heigth++
184 | }
185 | return heigth
186 | }
187 |
188 | func (c *graphCluster) getWidth() int {
189 | if c.cachedWidth >= 0 {
190 | return c.cachedWidth
191 | }
192 |
193 | membersWithDeps := 1
194 | for membersWithDeps < len(c.members) && c.members[len(c.members)-1-membersWithDeps].Successors().Len() > 0 {
195 | membersWithDeps++
196 | }
197 |
198 | clusterWidth := int(math.Floor(math.Sqrt(float64(len(c.members)))))
199 | if membersWithDeps > clusterWidth {
200 | clusterWidth = membersWithDeps
201 | }
202 | c.cachedWidth = clusterWidth
203 | return clusterWidth
204 | }
205 |
206 | func nodeNameToHash(nodeName string) string {
207 | var hash string
208 | for _, c := range nodeName {
209 | if unicode.IsOneOf(alphaNumericalRange, c) {
210 | hash += string(c)
211 | } else {
212 | hash += "_"
213 | }
214 | }
215 | return hash
216 | }
217 |
--------------------------------------------------------------------------------
/internal/printer/printer.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "os"
7 | "strings"
8 |
9 | "go.uber.org/zap"
10 |
11 | "github.com/Helcaraxan/gomod/internal/depgraph"
12 | "github.com/Helcaraxan/gomod/internal/graph"
13 | "github.com/Helcaraxan/gomod/internal/logger"
14 | "github.com/Helcaraxan/gomod/internal/util"
15 | )
16 |
17 | type Level uint8
18 |
19 | const (
20 | LevelModules Level = iota
21 | LevelPackages
22 | )
23 |
24 | // PrintConfig allows for the specification of parameters that should be passed to the Print
25 | // function of a Graph.
26 | type PrintConfig struct {
27 | // Logger that should be used to show progress while printing the Graph.
28 | Log *logger.Logger
29 |
30 | // Which level of granularity to print the graph at (modules, packages).
31 | Granularity Level
32 |
33 | // Annotate edges and nodes with their respective versions.
34 | Annotate bool
35 | // Path at which the printed version of the Graph should be stored. If set to a nil-string a
36 | // temporary file will be created.
37 | OutputPath string
38 | // Options for generating a visual representation of the Graph. If the field is non-nil, print
39 | // out an image file using GraphViz, if false print out the graph in DOT format.
40 | Style *StyleOptions
41 | }
42 |
43 | type StyleOptions struct {
44 | // Scale nodes according to the number of their successors and predecssors.
45 | ScaleNodes bool
46 | // Level at which to cluster nodes in the printed graph. This can be very beneficial for larger
47 | // dependency graphs that might be unreadable with the default settings.
48 | Cluster ClusterLevel
49 | }
50 |
51 | // Level at which to performing clustering when generating the image of the
52 | // dependency graph.
53 | type ClusterLevel int
54 |
55 | const (
56 | // No clustering. Each node is printed as is.
57 | Off ClusterLevel = iota
58 | // Cluster nodes that have the same parent.
59 | Parent
60 | // Cluster nodes that all have the same, unique, predecessor in the graph.
61 | Shared
62 | // Cluster nodes that all have the same (group of) predecessor(s) in the graph.
63 | Full
64 | )
65 |
66 | // Print takes in a PrintConfig struct and dumps the content of a HierarchicalDigraph instance
67 | // according to parameters.
68 | func Print(g *graph.HierarchicalDigraph, config *PrintConfig) error {
69 | var err error
70 | out := os.Stdout
71 | if len(config.OutputPath) > 0 {
72 | if out, err = util.PrepareOutputPath(config.Log, config.OutputPath); err != nil {
73 | return err
74 | }
75 | defer func() {
76 | _ = out.Close()
77 | }()
78 | config.Log.Debug("Writing DOT graph.", zap.String("path", config.OutputPath))
79 | } else {
80 | config.Log.Debug("Writing DOT graph to terminal.")
81 | }
82 |
83 | fileContent := []string{
84 | "strict digraph {",
85 | }
86 | fileContent = append(fileContent, determineGlobalOptions(g, config)...)
87 |
88 | clusters := computeGraphClusters(g, config)
89 | for _, cluster := range clusters.clusterList {
90 | fileContent = append(fileContent, printClusterToDot(cluster, config))
91 | }
92 |
93 | for _, node := range g.GetLevel(int(config.Granularity)).List() {
94 | fileContent = append(fileContent, printEdgesToDot(config, node, clusters)...)
95 | }
96 |
97 | fileContent = append(fileContent, "}")
98 |
99 | if _, err = out.WriteString(strings.Join(fileContent, "\n") + "\n"); err != nil {
100 | config.Log.Error("Failed to write DOT file.", zap.Error(err))
101 | return fmt.Errorf("could not write to %q", out.Name())
102 | }
103 | return nil
104 | }
105 |
106 | func determineGlobalOptions(g *graph.HierarchicalDigraph, config *PrintConfig) []string {
107 | globalOptions := []string{
108 | " node [shape=box,style=\"rounded,filled\"]",
109 | " start=0", // Needed for placement determinism.
110 | }
111 |
112 | if config.Annotate {
113 | globalOptions = append(globalOptions, " concentrate=true")
114 | } else {
115 | // Unfortunately we cannot use the "concentrate" option with 'ortho' splines as it leads to segfaults on large graphs.
116 | globalOptions = append(
117 | globalOptions,
118 | " splines=ortho", // By far the most readable form of splines on larger graphs but incompatible with annotations.
119 | )
120 | }
121 |
122 | if config.Style != nil {
123 | if config.Style.Cluster > Off {
124 | globalOptions = append(
125 | globalOptions,
126 | " graph [style=rounded]",
127 | " compound=true", // Needed for edges targeted at subgraphs.
128 | )
129 | }
130 | if config.Style.ScaleNodes {
131 | rankSep := math.Log10(float64(g.GetLevel(int(config.Granularity)).Len())) - 1
132 | if rankSep < 0.3 {
133 | rankSep = 0.3
134 | }
135 | globalOptions = append(globalOptions, fmt.Sprintf(" ranksep=%.2f", rankSep))
136 | }
137 | }
138 |
139 | return globalOptions
140 | }
141 |
142 | func printClusterToDot(cluster *graphCluster, config *PrintConfig) string {
143 | if len(cluster.members) == 0 {
144 | config.Log.Warn("Found an empty node cluster associated with.", zap.String("cluster", cluster.name()), zap.String("hash", cluster.hash))
145 | return ""
146 | } else if len(cluster.members) == 1 {
147 | return printNodeToDot(config, cluster.members[0])
148 | }
149 |
150 | dot := " subgraph " + cluster.name() + "{\n"
151 | for _, node := range cluster.members {
152 | dot += " " + printNodeToDot(config, node) + "\n"
153 | }
154 |
155 | // Print invisible nodes and edges that help node placement by forcing a grid layout.
156 | dot += " // The nodes and edges part of this subgraph defined below are only used to\n"
157 | dot += " // improve node placement but do not reflect actual dependencies.\n"
158 | dot += " node [style=invis]\n"
159 | dot += " edge [style=invis,minlen=1]\n"
160 | dot += " graph [color=blue]\n" //nolint:misspell
161 |
162 | rowSize := cluster.getWidth()
163 | firstRowSize := len(cluster.members) % rowSize
164 | firstRowOffset := (rowSize - firstRowSize) / 2
165 | if firstRowSize > 0 {
166 | for idx := 0; idx < firstRowOffset; idx++ {
167 | dot += fmt.Sprintf(" \"%s_%d\"\n", cluster.name(), idx)
168 | dot += fmt.Sprintf(" \"%s_%d\" -> \"%s\"\n", cluster.name(), idx, cluster.members[idx+firstRowSize].Name())
169 | }
170 | for idx := firstRowOffset + firstRowSize; idx < rowSize; idx++ {
171 | dot += fmt.Sprintf(" \"%s_%d\"\n", cluster.name(), idx)
172 | dot += fmt.Sprintf(" \"%s_%d\" -> \"%s\"\n", cluster.name(), idx, cluster.members[idx+firstRowSize].Name())
173 | }
174 | }
175 | for idx := 0; idx < firstRowSize; idx++ {
176 | dot += fmt.Sprintf(" \"%s\" -> \"%s\"\n", cluster.members[idx].Name(), cluster.members[idx+firstRowOffset+firstRowSize].Name())
177 | }
178 | for idx := firstRowSize; idx < len(cluster.members); idx++ {
179 | if idx+rowSize < len(cluster.members) {
180 | dot += fmt.Sprintf(" \"%s\" -> \"%s\"\n", cluster.members[idx].Name(), cluster.members[idx+rowSize].Name())
181 | }
182 | }
183 | return dot + " }"
184 | }
185 |
186 | type annotated interface {
187 | NodeAttributes(annotate bool) []string
188 | EdgeAttributes(target graph.Node, annotate bool) []string
189 | }
190 |
191 | var (
192 | _ annotated = &depgraph.Module{}
193 | _ annotated = &depgraph.Package{}
194 | )
195 |
196 | func printNodeToDot(config *PrintConfig, node graph.Node) string {
197 | var nodeOptions []string
198 | if config.Style != nil && config.Style.ScaleNodes {
199 | scaling := math.Log2(float64(node.Predecessors().Len()+node.Successors().Len())) / 5
200 | if scaling < 0.1 {
201 | scaling = 0.1
202 | }
203 | nodeOptions = append(nodeOptions, fmt.Sprintf("width=%.2f,height=%.2f", 5*scaling, scaling))
204 | }
205 |
206 | if a, ok := node.(annotated); ok {
207 | nodeOptions = append(nodeOptions, a.NodeAttributes(config.Annotate)...)
208 | }
209 |
210 | dot := " \"" + node.Name() + "\""
211 | if len(nodeOptions) > 0 {
212 | dot += " [" + strings.Join(nodeOptions, ",") + "]"
213 | }
214 | return dot
215 | }
216 |
217 | func printEdgesToDot(config *PrintConfig, node graph.Node, clusters *graphClusters) []string {
218 | clustersReached := map[int]struct{}{}
219 |
220 | var dots []string
221 | for _, dep := range node.Successors().List() {
222 | cluster, ok := clusters.clusterMap[dep.Hash()]
223 | if !ok {
224 | config.Log.Error("No cluster reference found for dependency.", zap.String("node", node.Hash()), zap.String("dep", dep.Hash()))
225 | continue
226 | } else if _, ok = clustersReached[cluster.id]; ok {
227 | continue
228 | }
229 | clustersReached[cluster.id] = struct{}{}
230 |
231 | target := dep.Name()
232 | var edgeAnnotations []string
233 | if minLength := clusters.clusterDepthMap(dep.Hash())[node.Hash()]; minLength > 1 {
234 | edgeAnnotations = append(edgeAnnotations, fmt.Sprintf("minlen=%d", minLength))
235 | }
236 |
237 | annotate := config.Annotate
238 | if len(cluster.members) > 1 {
239 | annotate = false
240 | target = cluster.getRepresentative()
241 | edgeAnnotations = append(edgeAnnotations, "lhead=\""+cluster.name()+"\"")
242 | }
243 |
244 | if a, ok := node.(annotated); ok {
245 | edgeAnnotations = append(edgeAnnotations, a.EdgeAttributes(dep, annotate)...)
246 | }
247 |
248 | dot := " \"" + node.Name() + "\" -> \"" + target + "\""
249 | if len(edgeAnnotations) > 0 {
250 | dot += " [" + strings.Join(edgeAnnotations, ",") + "]"
251 | }
252 | dots = append(dots, dot)
253 | }
254 | return dots
255 | }
256 |
--------------------------------------------------------------------------------
/internal/query/grammar.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type Expr interface {
9 | String() string
10 | Pos() Position
11 | _expr()
12 | }
13 |
14 | type ValueExpr interface {
15 | Expr
16 | _valueExpr()
17 | }
18 |
19 | type ExprBool struct {
20 | v bool
21 | p Position
22 | }
23 | type ExprInteger struct {
24 | v int
25 | p Position
26 | }
27 | type ExprString struct {
28 | v string
29 | p Position
30 | }
31 |
32 | func (e *ExprBool) Value() bool { return e.v }
33 | func (e *ExprInteger) Value() int { return e.v }
34 | func (e *ExprString) Value() string { return e.v }
35 | func (e *ExprBool) String() string { return fmt.Sprintf("%v", e.v) }
36 | func (e *ExprInteger) String() string { return fmt.Sprintf("%v", e.v) }
37 | func (e *ExprString) String() string { return fmt.Sprintf("%v", e.v) }
38 | func (e *ExprBool) Pos() Position { return e.p }
39 | func (e *ExprInteger) Pos() Position { return e.p }
40 | func (e *ExprString) Pos() Position { return e.p }
41 | func (e *ExprBool) _expr() {}
42 | func (e *ExprInteger) _expr() {}
43 | func (e *ExprString) _expr() {}
44 | func (e *ExprBool) _valueExpr() {}
45 | func (e *ExprInteger) _valueExpr() {}
46 | func (e *ExprString) _valueExpr() {}
47 |
48 | type BinaryExpr interface {
49 | Expr
50 | Operands() *BinaryOperands
51 | }
52 | type BinaryOperands struct {
53 | LHS Expr
54 | RHS Expr
55 | }
56 |
57 | type ExprDelta struct {
58 | BinaryOperands
59 | p Position
60 | }
61 | type ExprIntersect struct {
62 | BinaryOperands
63 | p Position
64 | }
65 | type ExprSubtract struct {
66 | BinaryOperands
67 | p Position
68 | }
69 | type ExprUnion struct {
70 | BinaryOperands
71 | p Position
72 | }
73 |
74 | func (e *ExprDelta) Operands() *BinaryOperands { return &e.BinaryOperands }
75 | func (e *ExprIntersect) Operands() *BinaryOperands { return &e.BinaryOperands }
76 | func (e *ExprSubtract) Operands() *BinaryOperands { return &e.BinaryOperands }
77 | func (e *ExprUnion) Operands() *BinaryOperands { return &e.BinaryOperands }
78 | func (e *ExprDelta) String() string { return fmt.Sprintf("(%v delta %v)", e.LHS, e.RHS) }
79 | func (e *ExprIntersect) String() string { return fmt.Sprintf("(%v inter %v)", e.LHS, e.RHS) }
80 | func (e *ExprSubtract) String() string { return fmt.Sprintf("(%v - %v)", e.LHS, e.RHS) }
81 | func (e *ExprUnion) String() string { return fmt.Sprintf("(%v + %v)", e.LHS, e.RHS) }
82 | func (e *ExprDelta) Pos() Position { return e.p }
83 | func (e *ExprIntersect) Pos() Position { return e.p }
84 | func (e *ExprSubtract) Pos() Position { return e.p }
85 | func (e *ExprUnion) Pos() Position { return e.p }
86 | func (e *ExprDelta) _expr() {}
87 | func (e *ExprIntersect) _expr() {}
88 | func (e *ExprSubtract) _expr() {}
89 | func (e *ExprUnion) _expr() {}
90 |
91 | type FuncExpr interface {
92 | Expr
93 | Name() string
94 | Args() ArgsListExpr
95 | }
96 |
97 | type ExprFunc struct {
98 | name string
99 | args ArgsListExpr
100 | p Position
101 | }
102 |
103 | func (e *ExprFunc) Name() string { return e.name }
104 | func (e *ExprFunc) Args() ArgsListExpr { return e.args }
105 | func (e *ExprFunc) String() string { return fmt.Sprintf("%s(%v)", e.name, e.args) }
106 | func (e *ExprFunc) Pos() Position { return e.p }
107 | func (e *ExprFunc) _expr() {}
108 |
109 | type ArgsListExpr interface {
110 | Expr
111 | Args() []Expr
112 | }
113 |
114 | type ExprArgsList struct {
115 | values []Expr
116 | p Position
117 | }
118 |
119 | func (e *ExprArgsList) Args() []Expr { return e.values }
120 | func (e *ExprArgsList) String() string {
121 | var strArgs []string
122 | for _, arg := range e.values {
123 | strArgs = append(strArgs, arg.String())
124 | }
125 | return fmt.Sprintf("[%s]", strings.Join(strArgs, ", "))
126 | }
127 | func (e *ExprArgsList) Pos() Position { return e.p }
128 | func (e *ExprArgsList) _expr() {}
129 |
130 | var (
131 | _ ValueExpr = &ExprBool{}
132 | _ ValueExpr = &ExprInteger{}
133 | _ ValueExpr = &ExprString{}
134 |
135 | _ BinaryExpr = &ExprSubtract{}
136 | _ BinaryExpr = &ExprUnion{}
137 | _ BinaryExpr = &ExprIntersect{}
138 | _ BinaryExpr = &ExprDelta{}
139 |
140 | _ FuncExpr = &ExprFunc{}
141 |
142 | _ ArgsListExpr = &ExprArgsList{}
143 | )
144 |
--------------------------------------------------------------------------------
/internal/query/parser_test.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 |
10 | "github.com/Helcaraxan/gomod/internal/testutil"
11 | )
12 |
13 | func TestParser(t *testing.T) {
14 | t.Parallel()
15 |
16 | testcases := map[string]struct {
17 | input string
18 | expectedExpr Expr
19 | }{
20 | "SimpleString": {
21 | input: "foo/bar",
22 | expectedExpr: &ExprString{v: "foo/bar"},
23 | },
24 | "SimpleOperator": {
25 | input: "foo/bar + dead/beef",
26 | expectedExpr: &ExprUnion{BinaryOperands: BinaryOperands{
27 | LHS: &ExprString{v: "foo/bar"},
28 | RHS: &ExprString{v: "dead/beef"},
29 | }},
30 | },
31 | "MultipleOperators": {
32 | input: "foo - bar delta dead + beef inter null",
33 | expectedExpr: &ExprSubtract{BinaryOperands: BinaryOperands{
34 | LHS: &ExprString{v: "foo"},
35 | RHS: &ExprUnion{BinaryOperands: BinaryOperands{
36 | LHS: &ExprDelta{BinaryOperands: BinaryOperands{
37 | LHS: &ExprString{v: "bar"},
38 | RHS: &ExprString{v: "dead"},
39 | }},
40 | RHS: &ExprIntersect{BinaryOperands: BinaryOperands{
41 | LHS: &ExprString{v: "beef"},
42 | RHS: &ExprString{v: "null"},
43 | }},
44 | }},
45 | }},
46 | },
47 | "OperatorsAndParenthesises": {
48 | input: "foo + bar delta (dead - beef) inter null",
49 | expectedExpr: &ExprUnion{BinaryOperands: BinaryOperands{
50 | LHS: &ExprString{v: "foo"},
51 | RHS: &ExprIntersect{BinaryOperands: BinaryOperands{
52 | LHS: &ExprDelta{BinaryOperands: BinaryOperands{
53 | LHS: &ExprString{v: "bar"},
54 | RHS: &ExprSubtract{BinaryOperands: BinaryOperands{
55 | LHS: &ExprString{v: "dead"},
56 | RHS: &ExprString{v: "beef"},
57 | }},
58 | }},
59 | RHS: &ExprString{v: "null"},
60 | }},
61 | }},
62 | },
63 | "SimpleFuncCallOneArg": {
64 | input: "deps(foo)",
65 | expectedExpr: &ExprFunc{
66 | name: "deps",
67 | args: &ExprArgsList{values: []Expr{
68 | &ExprString{v: "foo"},
69 | }},
70 | },
71 | },
72 | "SimpleFuncCallTwoArgs": {
73 | input: "deps(foo, 7)",
74 | expectedExpr: &ExprFunc{
75 | name: "deps",
76 | args: &ExprArgsList{values: []Expr{
77 | &ExprString{v: "foo"},
78 | &ExprInteger{v: 7},
79 | }},
80 | },
81 | },
82 | "SimpleFuncCallThreeArgs": {
83 | input: "deps(foo, 42, true)",
84 | expectedExpr: &ExprFunc{
85 | name: "deps",
86 | args: &ExprArgsList{values: []Expr{
87 | &ExprString{v: "foo"},
88 | &ExprInteger{v: 42},
89 | &ExprBool{v: true},
90 | }},
91 | },
92 | },
93 | "NestedFuncCalls": {
94 | input: "foo(bar(test))",
95 | expectedExpr: &ExprFunc{
96 | name: "foo",
97 | args: &ExprArgsList{values: []Expr{
98 | &ExprFunc{
99 | name: "bar",
100 | args: &ExprArgsList{values: []Expr{
101 | &ExprString{v: "test"},
102 | }},
103 | },
104 | }},
105 | },
106 | },
107 | "ComplexFuncCall": {
108 | input: "foo((bar - dead) delta beef + null, 3, true)",
109 | expectedExpr: &ExprFunc{
110 | name: "foo",
111 | args: &ExprArgsList{values: []Expr{
112 | &ExprUnion{BinaryOperands: BinaryOperands{
113 | LHS: &ExprDelta{BinaryOperands: BinaryOperands{
114 | LHS: &ExprSubtract{BinaryOperands: BinaryOperands{
115 | LHS: &ExprString{v: "bar"},
116 | RHS: &ExprString{v: "dead"},
117 | }},
118 | RHS: &ExprString{v: "beef"},
119 | }},
120 | RHS: &ExprString{v: "null"},
121 | }},
122 | &ExprInteger{v: 3},
123 | &ExprBool{v: true},
124 | }},
125 | },
126 | },
127 | "ComplexExpression": {
128 | input: "deps(foo) inter (rdeps(bar, 5, true) + dead) - beef",
129 | expectedExpr: &ExprSubtract{BinaryOperands: BinaryOperands{
130 | LHS: &ExprIntersect{BinaryOperands: BinaryOperands{
131 | LHS: &ExprFunc{
132 | name: "deps",
133 | args: &ExprArgsList{values: []Expr{
134 | &ExprString{v: "foo"},
135 | }},
136 | },
137 | RHS: &ExprUnion{BinaryOperands: BinaryOperands{
138 | LHS: &ExprFunc{
139 | name: "rdeps",
140 | args: &ExprArgsList{values: []Expr{
141 | &ExprString{v: "bar"},
142 | &ExprInteger{v: 5},
143 | &ExprBool{v: true},
144 | }},
145 | },
146 | RHS: &ExprString{v: "dead"},
147 | }},
148 | }},
149 | RHS: &ExprString{v: "beef"},
150 | }},
151 | },
152 | }
153 |
154 | for name := range testcases {
155 | testcase := testcases[name]
156 | t.Run(name, func(t *testing.T) {
157 | t.Parallel()
158 |
159 | expr, err := Parse(testutil.TestLogger(t), testcase.input)
160 | require.NoError(t, err)
161 | assert.Equal(t, testcase.expectedExpr.String(), expr.String())
162 | })
163 | }
164 | }
165 |
166 | func TestParserErrors(t *testing.T) {
167 | testcases := map[string]struct {
168 | input string
169 | expectedErr error
170 | }{
171 | "EmptyExpression": {
172 | input: "",
173 | expectedErr: ErrEmptyExpression,
174 | },
175 | "EmptyFuncCall": {
176 | input: "foo()",
177 | expectedErr: ErrEmptyFuncCall,
178 | },
179 | "EmptyParenthesis": {
180 | input: "()",
181 | expectedErr: ErrEmptyParenthesis,
182 | },
183 | "MissingArgument": {
184 | input: "bar, foo,",
185 | expectedErr: ErrMissingArgument,
186 | },
187 | "MissingOperator": {
188 | input: "foo bar",
189 | expectedErr: ErrMissingOperator,
190 | },
191 | "InvalidFuncName": {
192 | input: "1(foo, 1, false)",
193 | expectedErr: ErrInvalidFuncName,
194 | },
195 | "UnexpectedComma": {
196 | input: ",",
197 | expectedErr: ErrUnexpectedComma,
198 | },
199 | "UnexpectedOperator": {
200 | input: "delta",
201 | expectedErr: ErrUnexpectedOperator,
202 | },
203 | "UnexpectedParenthesis": {
204 | input: "(bar))",
205 | expectedErr: ErrUnexpectedParenthesis,
206 | },
207 | "MissingOperand": {
208 | input: "foo union bar -",
209 | expectedErr: ErrMissingArgument,
210 | },
211 | "InvalidOperandLHS": {
212 | input: "false inter bar",
213 | expectedErr: ErrInvalidArgument,
214 | },
215 | "InvalidOperandRHS": {
216 | input: "foo delta 3",
217 | expectedErr: ErrInvalidArgument,
218 | },
219 | }
220 |
221 | for name := range testcases {
222 | testcase := testcases[name]
223 | t.Run(name, func(t *testing.T) {
224 | t.Parallel()
225 |
226 | expr, err := Parse(testutil.TestLogger(t), testcase.input)
227 | assert.True(t, errors.Is(err, testcase.expectedErr), err)
228 | assert.Nil(t, expr)
229 | })
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/internal/query/tokenizer.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "strconv"
8 | "strings"
9 | "unicode"
10 | )
11 |
12 | var ErrUnclosedString = errors.New("missing quotes to close of a string")
13 |
14 | type unclosedStringErr struct {
15 | str string
16 | pos Position
17 | }
18 |
19 | func (e *unclosedStringErr) Error() string {
20 | return fmt.Sprintf("unclosed string at position %v: %s", e.pos, e.str)
21 | }
22 |
23 | func (e *unclosedStringErr) Unwrap() error {
24 | return ErrUnclosedString
25 | }
26 |
27 | var ErrTokenizer = errors.New("unexpected tokenizer error")
28 |
29 | type tokenizerErr struct {
30 | err error
31 | pos Position
32 | }
33 |
34 | func (e *tokenizerErr) Error() string {
35 | return fmt.Sprintf("tokenizer error at position %v: %v", e.pos, e.err)
36 | }
37 |
38 | func (e *tokenizerErr) Unwrap() error {
39 | return ErrTokenizer
40 | }
41 |
42 | type tokenizer struct {
43 | s *strings.Reader
44 | }
45 |
46 | func newTokenizer(s string) *tokenizer {
47 | return &tokenizer{s: strings.NewReader(s)}
48 | }
49 |
50 | // nolint: gocyclo
51 | func (t *tokenizer) next() (tkn token, err error) {
52 | var s tokenString
53 | var r rune
54 | var p int64
55 |
56 | for {
57 | p = t.s.Size() - int64(t.s.Len())
58 | r, _, err = t.s.ReadRune()
59 | if err == io.EOF {
60 | return nil, io.EOF
61 | } else if err != nil {
62 | return nil, &tokenizerErr{err: err, pos: pos(p, t.s.Size()-int64(t.s.Len()))}
63 | }
64 | if !unicode.IsSpace(r) {
65 | break
66 | }
67 | }
68 |
69 | switch r {
70 | // Special characters.
71 | case '(':
72 | return &tokenParenLeft{p: pos(p, p+1)}, nil
73 | case ')':
74 | return &tokenParenRight{p: pos(p, p+1)}, nil
75 | case '-':
76 | return &tokenSubtract{p: pos(p, p+1)}, nil
77 | case '+':
78 | return &tokenUnion{p: pos(p, p+1)}, nil
79 | case ',':
80 | return &tokenComma{p: pos(p, p+1)}, nil
81 |
82 | // Quoted string.
83 | case '"', '\'':
84 | s, err = readString(t.s, string(r))
85 | if err != nil {
86 | return nil, &tokenizerErr{err: err, pos: pos(p, t.s.Size()-int64(t.s.Len()))}
87 | }
88 | if _, _, err = t.s.ReadRune(); err == io.EOF {
89 | return nil, &unclosedStringErr{str: string(s.v), pos: pos(p, s.p.end)}
90 | } else if err != nil {
91 | return nil, &tokenizerErr{err: err, pos: pos(p, t.s.Size()-int64(t.s.Len()))}
92 | }
93 | s.p = pos(s.p.start-1, s.p.end+1)
94 | return &s, nil
95 |
96 | // String-based token.
97 | default:
98 | if err = t.s.UnreadRune(); err != nil {
99 | return nil, &tokenizerErr{err: err, pos: pos(p, t.s.Size()-int64(t.s.Len()))}
100 | }
101 |
102 | s, err := readString(t.s, "()=,\"' \t\n")
103 | if err != nil {
104 | return nil, &tokenizerErr{err: err, pos: pos(p, t.s.Size()-int64(t.s.Len()))}
105 | }
106 |
107 | switch s.v {
108 | case "true":
109 | return &tokenBoolean{p: pos(p, p+4), v: true}, nil
110 | case "false":
111 | return &tokenBoolean{p: pos(p, p+5), v: false}, nil
112 | case "minus":
113 | return &tokenSubtract{p: pos(p, p+5)}, nil
114 | case "union":
115 | return &tokenUnion{p: pos(p, p+5)}, nil
116 | case "inter":
117 | return &tokenIntersect{p: pos(p, p+5)}, nil
118 | case "delta":
119 | return &tokenDelta{p: pos(p, p+5)}, nil
120 | default:
121 | if v, intErr := strconv.Atoi(string(s.v)); intErr == nil {
122 | return &tokenInteger{
123 | p: s.p,
124 | v: v,
125 | }, nil
126 | }
127 | return &s, nil
128 | }
129 | }
130 | }
131 |
132 | func readString(s *strings.Reader, eos string) (tokenString, error) {
133 | acc := strings.Builder{}
134 | p := s.Size() - int64(s.Len())
135 | for {
136 | r, _, err := s.ReadRune()
137 | if err == io.EOF {
138 | return tokenString{
139 | p: pos(p, s.Size()),
140 | v: acc.String(),
141 | }, nil
142 | } else if err != nil {
143 | return tokenString{}, err
144 | }
145 |
146 | if strings.ContainsRune(eos, r) {
147 | if err = s.UnreadRune(); err != nil {
148 | return tokenString{}, err
149 | }
150 | return tokenString{
151 | p: pos(p, s.Size()-int64(s.Len())),
152 | v: acc.String(),
153 | }, nil
154 | }
155 |
156 | if _, err = acc.WriteRune(r); err != nil {
157 | return tokenString{}, err
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/internal/query/tokenizer_test.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestSingleTokens(t *testing.T) {
12 | t.Parallel()
13 |
14 | testcases := map[string]struct {
15 | input string
16 | expectedToken token
17 | expectedErr error
18 | }{
19 | "StringSimple": {
20 | input: "foo",
21 | expectedToken: &tokenString{p: pos(0, 3), v: "foo"},
22 | expectedErr: nil,
23 | },
24 | "StringWithInteger": {
25 | input: "42foo",
26 | expectedToken: &tokenString{p: pos(0, 5), v: "42foo"},
27 | expectedErr: nil,
28 | },
29 | "StringWithSpecialCharacters": {
30 | input: "foo-bar+dead",
31 | expectedToken: &tokenString{p: pos(0, 12), v: "foo-bar+dead"},
32 | expectedErr: nil,
33 | },
34 | "StringQuotedDouble": {
35 | input: "\"foo\"",
36 | expectedToken: &tokenString{p: pos(0, 5), v: "foo"},
37 | expectedErr: nil,
38 | },
39 | "StringQuotedSingle": {
40 | input: "'foo'",
41 | expectedToken: &tokenString{p: pos(0, 5), v: "foo"},
42 | expectedErr: nil,
43 | },
44 | "StringQuotedDoubleUnclosed": {
45 | input: "\"foo",
46 | expectedToken: nil,
47 | expectedErr: ErrUnclosedString,
48 | },
49 | "StringQuotedSingleUnclosed": {
50 | input: "'foo",
51 | expectedToken: nil,
52 | expectedErr: ErrUnclosedString,
53 | },
54 | "Integer": {
55 | input: "42",
56 | expectedToken: &tokenInteger{p: pos(0, 2), v: 42},
57 | expectedErr: nil,
58 | },
59 | "SubSign": {
60 | input: "-",
61 | expectedToken: &tokenSubtract{p: pos(0, 1)},
62 | expectedErr: nil,
63 | },
64 | "SubString": {
65 | input: "minus",
66 | expectedToken: &tokenSubtract{p: pos(0, 5)},
67 | expectedErr: nil,
68 | },
69 | "UnionSign": {
70 | input: "+",
71 | expectedToken: &tokenUnion{p: pos(0, 1)},
72 | expectedErr: nil,
73 | },
74 | "UnionString": {
75 | input: "union",
76 | expectedToken: &tokenUnion{p: pos(0, 5)},
77 | expectedErr: nil,
78 | },
79 | "Inter": {
80 | input: "inter",
81 | expectedToken: &tokenIntersect{p: pos(0, 5)},
82 | expectedErr: nil,
83 | },
84 | "Delta": {
85 | input: "delta",
86 | expectedToken: &tokenDelta{p: pos(0, 5)},
87 | expectedErr: nil,
88 | },
89 | "ParenthesisLeft": {
90 | input: "(",
91 | expectedToken: &tokenParenLeft{p: pos(0, 1)},
92 | expectedErr: nil,
93 | },
94 | "ParenthesisRight": {
95 | input: ")",
96 | expectedToken: &tokenParenRight{p: pos(0, 1)},
97 | expectedErr: nil,
98 | },
99 | "Comma": {
100 | input: ",",
101 | expectedToken: &tokenComma{p: pos(0, 1)},
102 | expectedErr: nil,
103 | },
104 | "True": {
105 | input: "true",
106 | expectedToken: &tokenBoolean{p: pos(0, 4), v: true},
107 | expectedErr: nil,
108 | },
109 | "False": {
110 | input: "false",
111 | expectedToken: &tokenBoolean{p: pos(0, 5), v: false},
112 | expectedErr: nil,
113 | },
114 | }
115 |
116 | for name := range testcases {
117 | testcase := testcases[name]
118 | t.Run(name, func(t *testing.T) {
119 | t.Parallel()
120 |
121 | tkn := newTokenizer(testcase.input)
122 | token, err := tkn.next()
123 | assert.Equal(t, testcase.expectedToken, token)
124 | assert.True(t, errors.Is(err, testcase.expectedErr))
125 |
126 | if testcase.expectedErr == nil {
127 | token, err = tkn.next()
128 | assert.Nil(t, token)
129 | assert.Equal(t, io.EOF, err)
130 | }
131 | })
132 | }
133 | }
134 |
135 | func TestTokenStream(t *testing.T) {
136 | t.Parallel()
137 |
138 | testcases := map[string]struct {
139 | input string
140 | expectedTokens []token
141 | expectedErr error
142 | }{
143 | "ParenthesizedExpression": {
144 | input: "deps(foo)",
145 | expectedTokens: []token{
146 | &tokenString{p: pos(0, 4), v: "deps"},
147 | &tokenParenLeft{p: pos(4, 5)},
148 | &tokenString{p: pos(5, 8), v: "foo"},
149 | &tokenParenRight{p: pos(8, 9)},
150 | },
151 | expectedErr: io.EOF,
152 | },
153 | "CommaSeparatedValues": {
154 | input: "foo, 42, bar",
155 | expectedTokens: []token{
156 | &tokenString{p: pos(0, 3), v: "foo"},
157 | &tokenComma{p: pos(3, 4)},
158 | &tokenInteger{p: pos(5, 7), v: 42},
159 | &tokenComma{p: pos(7, 8)},
160 | &tokenString{p: pos(9, 12), v: "bar"},
161 | },
162 | expectedErr: io.EOF,
163 | },
164 | "ParenthesizedExpressionComplex": {
165 | input: `deps("foo", 2, true) union rdeps(bar - dead/beef)`,
166 | expectedTokens: []token{
167 | &tokenString{p: pos(0, 4), v: "deps"},
168 | &tokenParenLeft{p: pos(4, 5)},
169 | &tokenString{p: pos(5, 10), v: "foo"},
170 | &tokenComma{p: pos(10, 11)},
171 | &tokenInteger{p: pos(12, 13), v: 2},
172 | &tokenComma{p: pos(13, 14)},
173 | &tokenBoolean{p: pos(15, 19), v: true},
174 | &tokenParenRight{p: pos(19, 20)},
175 | &tokenUnion{p: pos(21, 26)},
176 | &tokenString{p: pos(27, 32), v: "rdeps"},
177 | &tokenParenLeft{p: pos(32, 33)},
178 | &tokenString{p: pos(33, 36), v: "bar"},
179 | &tokenSubtract{p: pos(37, 38)},
180 | &tokenString{p: pos(39, 48), v: "dead/beef"},
181 | &tokenParenRight{p: pos(48, 49)},
182 | },
183 | expectedErr: io.EOF,
184 | },
185 | "UnclosedString": {
186 | input: `rdeps union( foo, "bar)`,
187 | expectedTokens: []token{
188 | &tokenString{p: pos(0, 5), v: "rdeps"},
189 | &tokenUnion{p: pos(6, 11)},
190 | &tokenParenLeft{p: pos(11, 12)},
191 | &tokenString{p: pos(13, 16), v: "foo"},
192 | &tokenComma{p: pos(16, 17)},
193 | },
194 | expectedErr: ErrUnclosedString,
195 | },
196 | }
197 |
198 | for name := range testcases {
199 | testcase := testcases[name]
200 | t.Run(name, func(t *testing.T) {
201 | t.Parallel()
202 |
203 | r := newTokenizer(testcase.input)
204 |
205 | var err error
206 | var tokens []token
207 | for {
208 | var tkn token
209 | tkn, err = r.next()
210 | if err != nil {
211 | break
212 | }
213 | tokens = append(tokens, tkn)
214 | }
215 | assert.Equal(t, testcase.expectedTokens, tokens)
216 | assert.True(t, errors.Is(err, testcase.expectedErr))
217 | })
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/internal/query/tokens.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import "fmt"
4 |
5 | type token interface {
6 | String() string
7 | Pos() Position
8 | _tokenImpl()
9 | }
10 |
11 | type Position struct {
12 | start int64
13 | end int64
14 | }
15 |
16 | func pos(s int64, e int64) Position {
17 | return Position{
18 | start: s,
19 | end: e,
20 | }
21 | }
22 |
23 | func (p *Position) String() string {
24 | if p.start != p.end {
25 | return fmt.Sprintf("%d-%d", p.start+1, p.end+1)
26 | }
27 | return fmt.Sprintf("%d", p.start+1)
28 | }
29 |
30 | type valueToken interface {
31 | token
32 | _valueTokenImpl()
33 | }
34 |
35 | type tokenBoolean struct {
36 | p Position
37 | v bool
38 | }
39 | type tokenInteger struct {
40 | p Position
41 | v int
42 | }
43 | type tokenString struct {
44 | p Position
45 | v string
46 | }
47 |
48 | func (t *tokenBoolean) Pos() Position { return t.p }
49 | func (t *tokenInteger) Pos() Position { return t.p }
50 | func (t *tokenString) Pos() Position { return t.p }
51 | func (t *tokenBoolean) String() string { return fmt.Sprintf("%t", t.v) }
52 | func (t *tokenInteger) String() string { return fmt.Sprintf("%d", t.v) }
53 | func (t *tokenString) String() string { return t.v }
54 | func (t *tokenBoolean) _tokenImpl() {}
55 | func (t *tokenInteger) _tokenImpl() {}
56 | func (t *tokenString) _tokenImpl() {}
57 | func (t *tokenBoolean) _valueTokenImpl() {}
58 | func (t *tokenInteger) _valueTokenImpl() {}
59 | func (t *tokenString) _valueTokenImpl() {}
60 |
61 | type punctuationToken interface {
62 | token
63 | _punctuationTokenImpl()
64 | }
65 |
66 | type tokenComma struct {
67 | p Position
68 | }
69 | type tokenParenLeft struct {
70 | p Position
71 | }
72 | type tokenParenRight struct {
73 | p Position
74 | }
75 |
76 | func (t *tokenComma) Pos() Position { return t.p }
77 | func (t *tokenParenLeft) Pos() Position { return t.p }
78 | func (t *tokenParenRight) Pos() Position { return t.p }
79 | func (t *tokenComma) String() string { return ", " }
80 | func (t *tokenParenLeft) String() string { return "(" }
81 | func (t *tokenParenRight) String() string { return ")" }
82 | func (t *tokenComma) _tokenImpl() {}
83 | func (t *tokenParenLeft) _tokenImpl() {}
84 | func (t *tokenParenRight) _tokenImpl() {}
85 | func (t *tokenComma) _punctuationTokenImpl() {}
86 | func (t *tokenParenLeft) _punctuationTokenImpl() {}
87 | func (t *tokenParenRight) _punctuationTokenImpl() {}
88 |
89 | type operatorToken interface {
90 | token
91 | _operatorTokenImpl()
92 | }
93 |
94 | type tokenDelta struct {
95 | p Position
96 | }
97 | type tokenIntersect struct {
98 | p Position
99 | }
100 | type tokenSubtract struct {
101 | p Position
102 | }
103 | type tokenUnion struct {
104 | p Position
105 | }
106 |
107 | func (t *tokenDelta) Pos() Position { return t.p }
108 | func (t *tokenIntersect) Pos() Position { return t.p }
109 | func (t *tokenSubtract) Pos() Position { return t.p }
110 | func (t *tokenUnion) Pos() Position { return t.p }
111 | func (t *tokenDelta) String() string { return " delta " }
112 | func (t *tokenIntersect) String() string { return " inter " }
113 | func (t *tokenSubtract) String() string { return " - " }
114 | func (t *tokenUnion) String() string { return " + " }
115 | func (t *tokenDelta) _tokenImpl() {}
116 | func (t *tokenIntersect) _tokenImpl() {}
117 | func (t *tokenSubtract) _tokenImpl() {}
118 | func (t *tokenUnion) _tokenImpl() {}
119 | func (t *tokenDelta) _operatorTokenImpl() {}
120 | func (t *tokenIntersect) _operatorTokenImpl() {}
121 | func (t *tokenSubtract) _operatorTokenImpl() {}
122 | func (t *tokenUnion) _operatorTokenImpl() {}
123 |
124 | var (
125 | _ valueToken = &tokenBoolean{}
126 | _ valueToken = &tokenInteger{}
127 | _ valueToken = &tokenString{}
128 |
129 | _ punctuationToken = &tokenComma{}
130 | _ punctuationToken = &tokenParenLeft{}
131 | _ punctuationToken = &tokenParenRight{}
132 |
133 | _ operatorToken = &tokenDelta{}
134 | _ operatorToken = &tokenIntersect{}
135 | _ operatorToken = &tokenSubtract{}
136 | _ operatorToken = &tokenUnion{}
137 | )
138 |
--------------------------------------------------------------------------------
/internal/reveal/replacements_test.go:
--------------------------------------------------------------------------------
1 | package reveal
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | "github.com/Helcaraxan/gomod/internal/depgraph"
11 | "github.com/Helcaraxan/gomod/internal/modules"
12 | "github.com/Helcaraxan/gomod/internal/testutil"
13 | )
14 |
15 | var (
16 | replaceA = Replacement{
17 | Offender: &modules.ModuleInfo{Path: "offender"},
18 | Original: "originalA",
19 | Override: "overrideA",
20 | Version: "v1.0.0",
21 | }
22 | replaceB = Replacement{
23 | Offender: moduleA,
24 | Original: "originalB",
25 | Override: "overrideB",
26 | Version: "v1.0.0",
27 | }
28 | replaceC = Replacement{
29 | Offender: moduleA,
30 | Original: "originalC",
31 | Override: "./overrideC",
32 | }
33 | replaceD = Replacement{
34 | Offender: moduleA,
35 | Original: "originalD",
36 | Override: "./overrideD",
37 | }
38 | replaceE = Replacement{
39 | Offender: &modules.ModuleInfo{Path: "offender-bis"},
40 | Original: "originalA",
41 | Override: "overrideA-bis",
42 | Version: "v2.0.0",
43 | }
44 | replaceF = Replacement{
45 | Offender: &modules.ModuleInfo{Path: "offender-tertio"},
46 | Original: "originalB",
47 | Override: "overrideB-bis",
48 | Version: "v2.0.0",
49 | }
50 |
51 | testReplacements = &Replacements{
52 | main: "test-module",
53 | topLevel: map[string]string{
54 | "originalA": "overrideA",
55 | "originalB": "overrideB-bis",
56 | },
57 | replacedModules: []string{
58 | "originalA",
59 | "originalB",
60 | "originalC",
61 | },
62 | originToReplace: map[string][]Replacement{
63 | "originalA": {replaceA, replaceE},
64 | "originalB": {replaceB, replaceF},
65 | "originalC": {replaceC},
66 | },
67 | }
68 |
69 | moduleA = &modules.ModuleInfo{
70 | Main: false,
71 | Path: "moduleA",
72 | Version: "v1.0.0",
73 | GoMod: filepath.Join("testdata", "moduleA", "go.mod"),
74 | }
75 | moduleB = &modules.ModuleInfo{
76 | Main: false,
77 | Path: filepath.Join("testdata", "moduleB"),
78 | Version: "v1.1.0",
79 | }
80 | moduleC = &modules.ModuleInfo{
81 | Main: false,
82 | Path: "moduleA",
83 | Version: "v0.1.0",
84 | Replace: moduleA,
85 | GoMod: "nowhere",
86 | }
87 | moduleD = &modules.ModuleInfo{
88 | Main: false,
89 | Path: "moduleD",
90 | Version: "v0.0.1",
91 | GoMod: "",
92 | }
93 | )
94 |
95 | func createTestGraph(t *testing.T) *depgraph.DepGraph {
96 | testGraph := depgraph.NewGraph(testutil.TestLogger(t).Log(), "", &modules.ModuleInfo{
97 | Main: true,
98 | Path: "test/module",
99 | GoMod: filepath.Join("testdata", "mainModule", "go.mod"),
100 | })
101 | for _, module := range []*modules.ModuleInfo{moduleA, moduleB, moduleC, moduleD} {
102 | testGraph.AddModule(module)
103 | }
104 | return testGraph
105 | }
106 |
107 | func Test_ParseReplaces(t *testing.T) {
108 | t.Parallel()
109 |
110 | testcases := map[string]struct {
111 | input string
112 | offender *modules.ModuleInfo
113 | expected []Replacement
114 | }{
115 | "SingleReplace": {
116 | input: "replace originalA => overrideA v1.0.0",
117 | offender: &modules.ModuleInfo{Path: "offender"},
118 | expected: []Replacement{replaceA},
119 | },
120 | "MultiReplace": {
121 | input: `
122 | replace (
123 | originalB => overrideB v1.0.0
124 | originalC => ./overrideC
125 | )
126 | `,
127 | offender: moduleA,
128 | expected: []Replacement{
129 | replaceB,
130 | replaceC,
131 | },
132 | },
133 | "MixedReplace": {
134 | input: `
135 | replace (
136 | originalB => overrideB v1.0.0
137 | originalC => ./overrideC
138 | )
139 |
140 | replace originalD => ./overrideD
141 | `,
142 | offender: moduleA,
143 | expected: []Replacement{
144 | replaceD,
145 | replaceB,
146 | replaceC,
147 | },
148 | },
149 | "FullGoMod": {
150 | input: `module github.com/foo/bar
151 |
152 | go = 1.12.5
153 |
154 | require (
155 | github.com/my-dep/A v1.2.0
156 | github.com/my-dep/B v1.9.2-201905291510-0123456789ab // indirect
157 | originalB v0.4.3
158 | originalC v0.2.3
159 | originalD v0.1.0
160 | )
161 |
162 | // Override this because it's upstream is broken.
163 | replace originalC => ./overrideC // Bar
164 |
165 | // Moar overrides.
166 | replace (
167 | // Foo.
168 | originalB => overrideB v1.0.0
169 | originalD => ./overrideD
170 | )
171 | `,
172 | offender: moduleA,
173 | expected: []Replacement{
174 | replaceC,
175 | replaceB,
176 | replaceD,
177 | },
178 | },
179 | }
180 |
181 | for name := range testcases {
182 | testcase := testcases[name]
183 | t.Run(name, func(t *testing.T) {
184 | t.Parallel()
185 |
186 | log := testutil.TestLogger(t)
187 | output := parseGoModForReplacements(log.Log(), testcase.offender, testcase.input)
188 | assert.Equal(t, testcase.expected, output)
189 | })
190 | }
191 | }
192 |
193 | func Test_FindReplacements(t *testing.T) {
194 | t.Parallel()
195 |
196 | expectedReplacements := &Replacements{
197 | main: "test/module",
198 | topLevel: map[string]string{"module/foo": "module/foo-bis"},
199 | replacedModules: []string{
200 | "originalB",
201 | "originalC",
202 | "originalD",
203 | },
204 | originToReplace: map[string][]Replacement{
205 | "originalB": {replaceB},
206 | "originalC": {replaceC},
207 | "originalD": {replaceD},
208 | },
209 | }
210 |
211 | replacements, err := FindReplacements(testutil.TestLogger(t).Log(), createTestGraph(t))
212 | assert.NoError(t, err, "Should not error while searching for replacements.")
213 | assert.Equal(t, expectedReplacements, replacements, "Should find the expected replacement information.")
214 | }
215 |
216 | func Test_FilterReplacements(t *testing.T) {
217 | t.Parallel()
218 |
219 | t.Run("OffenderEmpty", func(t *testing.T) {
220 | filtered := testReplacements.FilterOnOffendingModule(nil)
221 | assert.Equal(t, testReplacements, filtered, "Should return an identical array.")
222 | })
223 | t.Run("Offender", func(t *testing.T) {
224 | filtered := testReplacements.FilterOnOffendingModule([]string{"offender", "pre-offender", "offender-post"})
225 | assert.Equal(t, &Replacements{
226 | main: "test-module",
227 | topLevel: map[string]string{
228 | "originalA": "overrideA",
229 | "originalB": "overrideB-bis",
230 | },
231 | replacedModules: []string{
232 | "originalA",
233 | },
234 | originToReplace: map[string][]Replacement{
235 | "originalA": {replaceA},
236 | },
237 | }, filtered, "Should filter out the expected replacements.")
238 | })
239 |
240 | t.Run("OriginsEmpty", func(t *testing.T) {
241 | filtered := testReplacements.FilterOnReplacedModule(nil)
242 | assert.Equal(t, testReplacements, filtered, "Should return an identical array.")
243 | })
244 | t.Run("Origins", func(t *testing.T) {
245 | filtered := testReplacements.FilterOnReplacedModule([]string{"originalA", "originalC", "not-original"})
246 | assert.Equal(t, &Replacements{
247 | main: "test-module",
248 | topLevel: map[string]string{
249 | "originalA": "overrideA",
250 | "originalB": "overrideB-bis",
251 | },
252 | replacedModules: []string{
253 | "originalA",
254 | "originalC",
255 | },
256 | originToReplace: map[string][]Replacement{
257 | "originalA": {replaceA, replaceE},
258 | "originalC": {replaceC},
259 | },
260 | }, filtered, "Should filter out the expected replacements.")
261 | })
262 | }
263 |
264 | func Test_PrintReplacements(t *testing.T) {
265 | t.Parallel()
266 | const expectedOutput = `'originalA' is replaced:
267 | ✓ offender -> overrideA @ v1.0.0
268 | offender-bis -> overrideA-bis @ v2.0.0
269 |
270 | 'originalB' is replaced:
271 | moduleA -> overrideB @ v1.0.0
272 | ✓ offender-tertio -> overrideB-bis @ v2.0.0
273 |
274 | 'originalC' is replaced:
275 | moduleA -> ./overrideC
276 |
277 | [✓] Match with a top-level replace in 'test-module'
278 | `
279 |
280 | writer := &strings.Builder{}
281 | testReplacements.Print(testutil.TestLogger(t).Log(), writer, nil, nil)
282 | assert.Equal(t, expectedOutput, writer.String(), "Should print the expected output.")
283 | }
284 |
285 | func Test_FindGoModFile(t *testing.T) {
286 | t.Parallel()
287 |
288 | testcases := map[string]struct {
289 | module *modules.ModuleInfo
290 | expectedModule *modules.ModuleInfo
291 | expectedPath string
292 | }{
293 | "NoModule": {
294 | module: nil,
295 | expectedModule: nil,
296 | expectedPath: "",
297 | },
298 | "Standard": {
299 | module: moduleA,
300 | expectedModule: moduleA,
301 | expectedPath: filepath.Join("testdata", "moduleA", "go.mod"),
302 | },
303 | "NoGoMod": {
304 | module: moduleB,
305 | expectedModule: moduleB,
306 | expectedPath: filepath.Join("testdata", "moduleB", "go.mod"),
307 | },
308 | "Replaced": {
309 | module: moduleC,
310 | expectedModule: moduleA,
311 | expectedPath: filepath.Join("testdata", "moduleA", "go.mod"),
312 | },
313 | "Invalid": {
314 | module: moduleD,
315 | expectedModule: moduleD,
316 | expectedPath: "",
317 | },
318 | }
319 |
320 | for name := range testcases {
321 | testcase := testcases[name]
322 | t.Run(name, func(t *testing.T) {
323 | t.Parallel()
324 |
325 | module, goModPath := findGoModFile(testutil.TestLogger(t).Log(), testcase.module)
326 | assert.Equal(t, testcase.expectedModule, module, "Should have determined the used module correctly.")
327 | assert.Equal(t, testcase.expectedPath, goModPath, "Should have determined the correct go.mod path.")
328 | })
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/internal/reveal/testdata/mainModule/go.mod:
--------------------------------------------------------------------------------
1 | module test/module
2 |
3 | go 1.12
4 |
5 | require (
6 | module/foo v1.0.0
7 | module/bar v1.0.0
8 | )
9 |
10 | replace module/foo => module/foo-bis v1.0.0
11 |
--------------------------------------------------------------------------------
/internal/reveal/testdata/moduleA/go.mod:
--------------------------------------------------------------------------------
1 | module moduleA
2 |
3 | go 1.12
4 |
5 | require (
6 | originalB v1.0.0
7 | originalC v1.0.0
8 | originalD v1.0.0
9 | )
10 |
11 | replace originalD => ./overrideD
12 |
13 | replace (
14 | originalB => overrideB v1.0.0
15 | originalC => ./overrideC
16 | )
17 |
--------------------------------------------------------------------------------
/internal/reveal/testdata/moduleB/go.mod:
--------------------------------------------------------------------------------
1 | module moduleB
2 |
3 | go 1.12
4 |
--------------------------------------------------------------------------------
/internal/testutil/log.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | "go.uber.org/zap/zapcore"
10 |
11 | "github.com/Helcaraxan/gomod/internal/logger"
12 | )
13 |
14 | func TestLogger(t *testing.T) *logger.Builder {
15 | out := &syncBuffer{Builder: strings.Builder{}}
16 | t.Cleanup(func() {
17 | if t.Failed() {
18 | fmt.Fprintf(os.Stderr, out.String())
19 | }
20 | })
21 |
22 | fmt.Fprintf(out, "--- Test %s ---", t.Name())
23 | dl := logger.NewBuilder(out)
24 | dl.SetDomainLevel("all", zapcore.DebugLevel)
25 | return dl
26 | }
27 |
28 | type syncBuffer struct {
29 | strings.Builder
30 | }
31 |
32 | func (s *syncBuffer) Sync() error { return nil }
33 |
--------------------------------------------------------------------------------
/internal/testutil/test_module.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | const fakeGoDriver = `#!/usr/bin/env bash
16 | set -e -u -o pipefail
17 |
18 | content_dir="%s"
19 |
20 | if [[ -f error.lock ]]; then
21 | echo >2& "deliberate fake go driver error"
22 | exit 1
23 | fi
24 |
25 | case "$1" in
26 | mod)
27 | cat "${content_dir}/graph-output.txt"
28 | ;;
29 | list)
30 | resource_type="pkg"
31 | for arg in ${@:2}; do
32 | if [[ ${arg} == "-m" ]]; then
33 | resource_type="mod"
34 | fi
35 | done
36 |
37 | files_to_print=()
38 | for arg in ${@:2}; do
39 | if [[ ${arg} == "all" ]]; then
40 | files_to_print=($(ls "${content_dir}/list-${resource_type}"-*.txt))
41 | break
42 | elif [[ ${arg:0:1} != "-" ]]; then
43 | files_to_print+=("${content_dir}/list-${resource_type}-${arg//\//_}.txt")
44 | fi
45 | done
46 | cat "${files_to_print[@]}"
47 | ;;
48 | *)
49 | echo >2& "Unrecognised command '$1' to fake go driver"
50 | exit 1
51 | ;;
52 | esac
53 | `
54 |
55 | type TestDefinition interface {
56 | GoDriverError() bool
57 | GoGraphOutput() string
58 | GoListPkgOutput() map[string]string
59 | GoListModOutput() map[string]string
60 | }
61 |
62 | func SetupTestModule(t *testing.T, testDefinitionPath string, testDefinition TestDefinition) string {
63 | tempDir := t.TempDir()
64 |
65 | require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, "go"), []byte(fmt.Sprintf(fakeGoDriver, tempDir)), 0700))
66 |
67 | currentEnvPath := os.Getenv("PATH")
68 | os.Setenv("PATH", fmt.Sprintf("%s:%s", tempDir, currentEnvPath))
69 |
70 | t.Cleanup(func() {
71 | require.NoError(t, os.Setenv("PATH", currentEnvPath))
72 | })
73 |
74 | raw, testErr := ioutil.ReadFile(testDefinitionPath)
75 | require.NoError(t, testErr)
76 | require.NoError(t, yaml.Unmarshal(raw, testDefinition))
77 |
78 | if testDefinition.GoDriverError() {
79 | require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, "error.lock"), []byte(""), 0600))
80 | return tempDir
81 | }
82 |
83 | require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, "graph-output.txt"), []byte(testDefinition.GoGraphOutput()), 0600))
84 | for mod, output := range testDefinition.GoListModOutput() {
85 | filename := fmt.Sprintf("list-mod-%s.txt", strings.ReplaceAll(mod, "/", "_"))
86 | require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, filename), []byte(output), 0600))
87 | }
88 | for pkg, output := range testDefinition.GoListPkgOutput() {
89 | filename := fmt.Sprintf("list-pkg-%s.txt", strings.ReplaceAll(pkg, "/", "_"))
90 | require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, filename), []byte(output), 0600))
91 | }
92 | return tempDir
93 | }
94 |
--------------------------------------------------------------------------------
/internal/util/exec.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "strings"
11 |
12 | "go.uber.org/zap"
13 |
14 | "github.com/Helcaraxan/gomod/internal/logger"
15 | )
16 |
17 | func RunCommand(log *logger.Logger, path string, cmd string, args ...string) (stdout []byte, stderr []byte, err error) {
18 | if !filepath.IsAbs(path) {
19 | if path, err = filepath.Abs(path); err != nil {
20 | return nil, nil, err
21 | }
22 | }
23 |
24 | stdoutBuffer := &bytes.Buffer{}
25 | stderrBuffer := &bytes.Buffer{}
26 |
27 | execCmd := exec.Command(cmd, args...)
28 | execCmd.Dir = path
29 | execCmd.Stdout = stdoutBuffer
30 | execCmd.Stderr = stderrBuffer
31 |
32 | if log.Core().Enabled(zap.DebugLevel) {
33 | execCmd.Stdout = io.MultiWriter(execCmd.Stdout, os.Stderr)
34 | execCmd.Stderr = io.MultiWriter(execCmd.Stderr, os.Stderr)
35 | }
36 |
37 | log.Debug("Running command.", zap.Strings("args", append([]string{execCmd.Path}, execCmd.Args...)))
38 | err = execCmd.Run()
39 | log.Debug(
40 | "Finished running.",
41 | zap.Strings("args", append([]string{execCmd.Path}, execCmd.Args...)),
42 | zap.ByteString("stdout", stdoutBuffer.Bytes()),
43 | zap.ByteString("stderr", stderrBuffer.Bytes()),
44 | )
45 | if err != nil {
46 | log.Error("Command exited with an error.", zap.Strings("args", append([]string{execCmd.Path}, execCmd.Args...)), zap.Error(err))
47 | return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), fmt.Errorf("failed to run '%s %s: %s", cmd, strings.Join(args, " "), err)
48 | }
49 | return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), nil
50 | }
51 |
--------------------------------------------------------------------------------
/internal/util/fileutil.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 |
8 | "go.uber.org/zap"
9 |
10 | "github.com/Helcaraxan/gomod/internal/logger"
11 | )
12 |
13 | // PrepareOutputPath ensures that the directory containing the specified path exist. It also checks
14 | // that the full path does not refer to an existing file or directory. If such a path already exists
15 | // an error is returned.
16 | func PrepareOutputPath(log *logger.Logger, outputPath string) (*os.File, error) {
17 | l := log.With(zap.String("output-path", outputPath))
18 | log.Debug("Preparing output path.")
19 |
20 | // Perform target file sanity checks.
21 | var sanityCheckErr error
22 | if _, err := os.Stat(outputPath); err == nil {
23 | l.Error("The specified output path already exists.")
24 | sanityCheckErr = fmt.Errorf("target file %q already exists", outputPath)
25 | } else if !os.IsNotExist(err) {
26 | l.Error("Failed to check if output path already exists.", zap.Error(err))
27 | sanityCheckErr = fmt.Errorf("could not stat about %q", outputPath)
28 | }
29 | if sanityCheckErr != nil {
30 | return nil, sanityCheckErr
31 | }
32 |
33 | l.Debug("Ensuring output path folder exists.")
34 | if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
35 | l.Error("Failed to create output directory.", zap.Error(err))
36 | return nil, fmt.Errorf("could not create %q", filepath.Dir(outputPath))
37 | }
38 | out, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0644)
39 | if err != nil {
40 | l.Error("Could not create output file.", zap.Error(err))
41 | return nil, err
42 | }
43 | return out, nil
44 | }
45 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 |
10 | "github.com/spf13/cobra"
11 | "go.uber.org/zap"
12 | "go.uber.org/zap/zapcore"
13 |
14 | "github.com/Helcaraxan/gomod/internal/analysis"
15 | "github.com/Helcaraxan/gomod/internal/depgraph"
16 | "github.com/Helcaraxan/gomod/internal/logger"
17 | "github.com/Helcaraxan/gomod/internal/parsers"
18 | "github.com/Helcaraxan/gomod/internal/printer"
19 | "github.com/Helcaraxan/gomod/internal/query"
20 | "github.com/Helcaraxan/gomod/internal/reveal"
21 | )
22 |
23 | type commonArgs struct {
24 | log *logger.Builder
25 | }
26 |
27 | func main() {
28 | var verbose []string
29 |
30 | commonArgs := &commonArgs{
31 | log: logger.NewBuilder(os.Stderr),
32 | }
33 |
34 | rootCmd := &cobra.Command{
35 | Use: "gomod",
36 | Short: gomodShort,
37 | Long: gomodLong,
38 | PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
39 | for _, domain := range verbose {
40 | commonArgs.log.SetDomainLevel(domain, zapcore.DebugLevel)
41 | }
42 |
43 | log := commonArgs.log.Domain(logger.InitDomain)
44 | if err := checkToolDependencies(log); err != nil {
45 | return err
46 | } else if err = checkGoModulePresence(log); err != nil {
47 | return err
48 | }
49 | return nil
50 | },
51 | }
52 |
53 | rootCmd.PersistentFlags().StringSliceVarP(
54 | &verbose,
55 | "verbose",
56 | "v",
57 | nil,
58 | "Verbose output. See 'gomod --help' for more information.",
59 | )
60 | v := rootCmd.Flag("verbose")
61 | v.NoOptDefVal = "all"
62 |
63 | rootCmd.AddCommand(
64 | initAnalyseCmd(commonArgs),
65 | initGraphCmd(commonArgs),
66 | initRevealCmd(commonArgs),
67 | initVersionCmd(commonArgs),
68 | )
69 |
70 | if err := rootCmd.Execute(); err != nil {
71 | commonArgs.log.Log().Debug("Exited with an error.", zap.Error(err))
72 | os.Exit(1)
73 | }
74 | }
75 |
76 | type graphArgs struct {
77 | *commonArgs
78 |
79 | annotate bool
80 | outputPath string
81 | packages bool
82 | style *printer.StyleOptions
83 |
84 | query string
85 | }
86 |
87 | func initGraphCmd(cArgs *commonArgs) *cobra.Command {
88 | cmdArgs := &graphArgs{
89 | commonArgs: cArgs,
90 | }
91 |
92 | var style string
93 | graphCmd := &cobra.Command{
94 | Use: "graph ",
95 | Short: graphShort,
96 | Long: graphLong,
97 | Args: cobra.MaximumNArgs(1),
98 | RunE: func(cmd *cobra.Command, args []string) error {
99 | if cmd.Flags().Changed("style") {
100 | styleOptions, err := parsers.ParseStyleConfiguration(cmdArgs.log.Domain(logger.InitDomain), style)
101 | if err != nil {
102 | return err
103 | }
104 | cmdArgs.style = styleOptions
105 | }
106 | if len(args) == 0 {
107 | cmdArgs.query = "**:test"
108 | } else {
109 | cmdArgs.query = args[0]
110 | }
111 | return runGraphCmd(cmdArgs)
112 | },
113 | }
114 |
115 | graphCmd.Flags().BoolVarP(&cmdArgs.annotate, "annotate", "a", false, "Annotate the graph's nodes and edges with version information")
116 | graphCmd.Flags().StringVarP(&cmdArgs.outputPath, "output", "o", "", "If set dump the output to this location")
117 | graphCmd.Flags().BoolVarP(&cmdArgs.packages, "packages", "p", false, "Operate at package-level instead of module-level on the dependency graph.")
118 | graphCmd.Flags().StringVar(&style, "style", "", "Set style options that add decorations and optimisations to the produced 'dot' output.")
119 |
120 | return graphCmd
121 | }
122 |
123 | func runGraphCmd(args *graphArgs) error {
124 | graph, err := depgraph.GetGraph(args.log, "")
125 | if err != nil {
126 | return err
127 | }
128 |
129 | q, err := query.Parse(args.log, args.query)
130 | if err != nil {
131 | return err
132 | }
133 | l := depgraph.LevelModules
134 | if args.packages {
135 | l = depgraph.LevelPackages
136 | }
137 | if err = graph.ApplyQuery(args.log, q, l); err != nil {
138 | return err
139 | }
140 | args.log.Log().Debug("Printing graph.")
141 | return printResult(graph, args)
142 | }
143 |
144 | type analyseArgs struct {
145 | *commonArgs
146 | }
147 |
148 | func initAnalyseCmd(cArgs *commonArgs) *cobra.Command {
149 | cmdArgs := &analyseArgs{
150 | commonArgs: cArgs,
151 | }
152 |
153 | analyseCmd := &cobra.Command{
154 | Use: "analyse",
155 | Aliases: []string{"analyze"}, // nolint
156 | Short: analyseShort,
157 | RunE: func(_ *cobra.Command, _ []string) error {
158 | return runAnalyseCmd(cmdArgs)
159 | },
160 | }
161 | return analyseCmd
162 | }
163 |
164 | func runAnalyseCmd(args *analyseArgs) error {
165 | graph, err := depgraph.GetGraph(args.log, "")
166 | if err != nil {
167 | return err
168 | }
169 | analysisResult, err := analysis.Analyse(args.log.Log(), graph)
170 | if err != nil {
171 | return err
172 | }
173 | return analysisResult.Print(os.Stdout)
174 | }
175 |
176 | type revealArgs struct {
177 | *commonArgs
178 | sources []string
179 | targets []string
180 | }
181 |
182 | func initRevealCmd(cArgs *commonArgs) *cobra.Command {
183 | cmdArgs := &revealArgs{
184 | commonArgs: cArgs,
185 | }
186 |
187 | revealCmd := &cobra.Command{
188 | Use: "reveal",
189 | Short: revealShort,
190 | RunE: func(_ *cobra.Command, _ []string) error {
191 | return runRevealCmd(cmdArgs)
192 | },
193 | }
194 |
195 | revealCmd.Flags().StringSliceVarP(&cmdArgs.sources, "sources", "s", nil, "Filter all places that are replacing dependencies.")
196 | revealCmd.Flags().StringSliceVarP(&cmdArgs.targets, "targets", "t", nil, "Filter all places that replace the specified modules.")
197 |
198 | return revealCmd
199 | }
200 |
201 | func runRevealCmd(args *revealArgs) error {
202 | graph, err := depgraph.GetGraph(args.log, "")
203 | if err != nil {
204 | return err
205 | }
206 | replacements, err := reveal.FindReplacements(args.log.Log(), graph)
207 | if err != nil {
208 | return err
209 | }
210 | return replacements.Print(args.log.Log(), os.Stdout, args.sources, args.targets)
211 | }
212 |
213 | type versionArgs struct {
214 | *commonArgs
215 | }
216 |
217 | func initVersionCmd(cArgs *commonArgs) *cobra.Command {
218 | cmdArgs := &versionArgs{
219 | commonArgs: cArgs,
220 | }
221 |
222 | versionCmd := &cobra.Command{
223 | Use: "version",
224 | Short: versionShort,
225 | RunE: func(_ *cobra.Command, _ []string) error {
226 | return runVersionCmd(cmdArgs)
227 | },
228 | }
229 |
230 | return versionCmd
231 | }
232 |
233 | func runVersionCmd(args *versionArgs) error {
234 | fmt.Printf("%s - built on %s from %s\n", version, date, commit)
235 | return nil
236 | }
237 |
238 | func checkToolDependencies(log *logger.Logger) error {
239 | tools := []string{
240 | "go",
241 | }
242 |
243 | success := true
244 | for _, tool := range tools {
245 | if _, err := exec.LookPath(tool); err != nil {
246 | success = false
247 | log.Error("A tool dependency does not seem to be available. Please install it first.", zap.String("tool", tool))
248 | }
249 | }
250 | if !success {
251 | return errors.New("missing tool dependencies")
252 | }
253 | return nil
254 | }
255 |
256 | func checkGoModulePresence(log *logger.Logger) error {
257 | path, err := os.Getwd()
258 | if err != nil {
259 | log.Error("Could not determine the current working directory.", zap.Error(err))
260 | return err
261 | }
262 |
263 | for {
264 | if _, err = os.Stat(filepath.Join(path, "go.mod")); err == nil {
265 | return nil
266 | }
267 | if path != filepath.VolumeName(path)+string(filepath.Separator) {
268 | break
269 | }
270 | }
271 | log.Error("This tool should be run from within a Go module.")
272 | return errors.New("missing go module")
273 | }
274 |
275 | func printResult(g *depgraph.DepGraph, args *graphArgs) error {
276 | l := printer.LevelModules
277 | if args.packages {
278 | l = printer.LevelPackages
279 | }
280 | return printer.Print(g.Graph, &printer.PrintConfig{
281 | Log: args.log.Domain(logger.PrinterDomain),
282 | Granularity: l,
283 | OutputPath: args.outputPath,
284 | Style: args.style,
285 | Annotate: args.annotate,
286 | })
287 | }
288 |
--------------------------------------------------------------------------------
/main_strings.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var (
4 | version = "devel-release"
5 | commit = "unknown-hash"
6 | date = "unknown-date"
7 | )
8 |
9 | const (
10 | gomodShort = "A tool to visualise and analyse a Go project's dependency graph."
11 | gomodLong = `A CLI tool for interacting with your Go project's dependency graph on various
12 | levels. See the online documentation or the '--help' for specific sub-commands to discover all the
13 | supported workflows.
14 |
15 | NB: The '--verbose' flag available on each command takes an optional list of strings that allow you
16 | to only get verbose output for specific pieces of logic. The available domains are:
17 | * init -> Code that runs before any actual dependency processing happens.
18 | * graph -> Interactions on the underlying graph representation used by 'gomod'.
19 | * modinfo -> Retrieval of information about all modules involved in the dependency graph.
20 | * pkginfo -> Retrieval of information about all packages involves in the dependency graph.
21 | * moddeps -> Retrieval of information about module-level dependency graph (e.g versions).
22 | * parser -> Parsing of user-specified dependency queries.
23 | * query -> Execution of a parsed user-query on the dependency graph.
24 | * printer -> Printing of the queried dependency graph.
25 | * all -> Covers all domains above.
26 | Without any arguments the behaviour defaults to enabling verbosity on all domains, the
27 | equivalent of passing 'all' as argument.
28 | `
29 |
30 | graphShort = "Visualise the dependency graph of a Go module."
31 | graphLong = `Generate a visualisation of the dependency network used by the code in your Go
32 | module.
33 |
34 | The command requires a query to be passed to determine what part of the graph
35 | should be printed. The query language itself supports the following syntax:
36 |
37 | - Exact or prefix path queries: foo.com/bar or foo.com/bar/...
38 | - Inclusion of test-only dependencies: test(foo.com/bar)
39 | - Dependency queries: 'deps(foo.com/bar)' or 'rdeps(foo.com/bar)
40 | - Depth-limited variants of the above: 'deps(foo.com/bar, 5)'
41 | - Recursive removal of single-parent leaf-nodes: shared(foo.com/bar)'
42 | - Various set operations: X + Y, X - Y, X inter Y, X delta Y.
43 |
44 | An example query:
45 |
46 | gomod graph -p 'deps(foo.com/bar/...) inter deps(test(test.io/pkg/tool))'
47 |
48 | The generated graph is colour and format coded:
49 | - Each module, or group of packages belonging to the same module, has a distinct
50 | colour.
51 | - Test-only dependencies are recognisable by the use of a lighter
52 | colour-palette.
53 | - Test-only edges are recognisable by a light blue colour.
54 | - Edges reflecting indirect module dependencies are marked with dashed instead
55 | of continuous lines.
56 |
57 | Other visual aspects (when run through the 'dot' tool) can be tuned with the
58 | '--style' flag. You can specify any formatting options as
59 |
60 | '