├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── example.png ├── go.mod ├── go.sum ├── integration ├── cli_test.go ├── test-author-txt.golden ├── test-txt-json.golden └── test-txt.golden ├── logo.png ├── main.go └── testdata ├── test-author.txt └── test.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | 5 | jobs: 6 | golangci: 7 | name: lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: golangci-lint 12 | uses: golangci/golangci-lint-action@v3 13 | with: 14 | # Optional: show only new issues if it's a pull request. The default value is `false`. 15 | only-new-issues: true 16 | # show file name and line numbers in output 17 | args: --timeout 15m0s --verbose --out-${NO_FUTURE}format colored-line-number -D errcheck 18 | 19 | - name: test 20 | run: go test -v ./... 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | name: Release 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.19 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v4 22 | with: 23 | version: latest 24 | args: release --clean 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .vscode/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - 3 | env: 4 | - CGO_ENABLED=0 5 | ldflags: 6 | - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.buildDate={{.Date}} 7 | 8 | archives: 9 | - 10 | format: binary 11 | 12 | checksum: 13 | name_template: 'checksums.txt' 14 | 15 | snapshot: 16 | name_template: "{{ .Tag }}-next" 17 | 18 | changelog: 19 | sort: asc 20 | filters: 21 | exclude: 22 | - '^docs:' 23 | - '^test:' 24 | - Merge pull request 25 | - Merge branch 26 | 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.4.0] - 2023-03-05 8 | ### Added 9 | - [#27](https://github.com/jakewarren/fixme/pull/26) Added support for JSON output 10 | 11 | ### Changed 12 | - Migrated to Github Actions. 13 | - Expanded test suite to test the JSON output and to ensure author tags are displayed correctly. 14 | 15 | ## [1.3.0] - 2019-10-30 16 | ### Added 17 | - [#3](https://github.com/jakewarren/fixme/pull/3) Added support for LaTeX comments. Thanks [@xarantolus](https://github.com/xarantolus)! 18 | 19 | ### Changed 20 | - Migrated to go modules. 21 | - Build releases with goreleaser. 22 | 23 | ## [1.2.0] - 2019-07-22 24 | ### Fixed 25 | - [#2](https://github.com/jakewarren/fixme/pull/2) Fixed an issue with output being inconsistent between runs. Thanks [@reidab](https://github.com/reidab)! 26 | 27 | ### Changed 28 | - If the user doesn't specify a directory, use the current directory as a default. 29 | - Exclude the vendor directories by default. 30 | 31 | ## [1.1.0] - 2017-10-03 32 | ### Changed 33 | - Added a max limit to the number of spawned goroutines to prevent trouble when running on large directories. 34 | 35 | ## [1.0.0] - 2017-10-03 36 | - Initial release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 jakewarren 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY := fixme 2 | 3 | 4 | # Build a development build 5 | build: 6 | @go build -o bin/${BINARY} 7 | 8 | # run tests 9 | test: 10 | @go test -v -race ./... 11 | 12 | # update the golden files used for the integration tests 13 | update-tests: 14 | @go test integration/cli_test.go -update 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fixme 2 | [![Build Status](https://github.com/jakewarren/fixme/workflows/lint/badge.svg)](https://github.com/jakewarren/fixme/actions) 3 | [![GitHub release](http://img.shields.io/github/release/jakewarren/fixme.svg?style=flat-square)](https://github.com/jakewarren/fixme/releases) 4 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/jakewarren/fixme/blob/master/LICENSE) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/jakewarren/fixme)](https://goreportcard.com/report/github.com/jakewarren/fixme) 6 | 7 | ![](logo.png) 8 | 9 | > Scan for NOTE, OPTIMIZE, TODO, HACK, XXX, FIXME, and BUG comments within your source, and print them to stdout so you can deal with them. 10 | 11 | ![](example.png) 12 | 13 | ## Table of Contents 14 | 15 | - [Background](#background) 16 | - [Install](#install) 17 | - [Usage](#usage) 18 | - [Maintainers](#maintainers) 19 | - [Contribute](#contribute) 20 | - [License](#license) 21 | 22 | ## Background 23 | 24 | Very heavily inspired by https://github.com/JohnPostlethwait/fixme. 25 | 26 | ## Install 27 | 28 | ### Option 1: Binary 29 | 30 | Download the latest release from [https://github.com/jakewarren/fixme/releases](https://github.com/jakewarren/fixme/releases) 31 | 32 | ### Option 2: From source 33 | 34 | ``` 35 | go install github.com/jakewarren/fixme@latest 36 | ``` 37 | 38 | ## Usage 39 | 40 | ``` 41 | ❯ fixme -h 42 | usage: fixme [] 43 | 44 | Searches for comment tags in code 45 | 46 | Optional flags: 47 | -h, --help Show context-sensitive help (also try --help-long and --help-man). 48 | --skip-hidden skip hidden folders (default=true) 49 | -i, --ignore-dir=vendor ... pattern of directories to ignore 50 | --ignore-exts=.txt ... pattern of file extensions to ignore 51 | --line-length-limit=1000 number of max characters in a line 52 | -l, --log-level=error log level (debug|info|error) 53 | -V, --version Show application version. 54 | 55 | Args: 56 | the file or directory to scan 57 | 58 | ``` 59 | 60 | ## Thanks to :heart: 61 | 62 | * [@xarantolus](https://github.com/xarantolus) 63 | * [@reidab](https://github.com/reidab) 64 | 65 | ## Maintainers 66 | 67 | [@jakewarren](https://github.com/jakewarren) 68 | 69 | ## Contribute 70 | 71 | PRs accepted. 72 | 73 | ## License 74 | 75 | MIT © 2017 Jake Warren 76 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakewarren/fixme/130d262cb63dcb67b8675431ecf37d05839d360a/example.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jakewarren/fixme 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 8 | github.com/apex/log v1.1.1 9 | github.com/cloudflare/ahocorasick v0.0.0-20131126104932-1ce46e42b741 10 | github.com/fatih/color v1.13.0 11 | github.com/remeh/sizedwaitgroup v1.0.0 12 | github.com/stretchr/testify v1.4.0 // indirect 13 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA= 6 | github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA= 7 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 8 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 9 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 10 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 11 | github.com/cloudflare/ahocorasick v0.0.0-20131126104932-1ce46e42b741 h1:8Xzh8Z+jiT/MpNO1RRu4/o4o3hP3iGWJaD5GfaH2Kak= 12 | github.com/cloudflare/ahocorasick v0.0.0-20131126104932-1ce46e42b741/go.mod h1:tGWUZLZp9ajsxUOnHmFFLnqnlKXsCn6GReG4jAD59H0= 13 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 16 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 17 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 18 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 19 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 24 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 25 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 26 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 27 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 28 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 29 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 30 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 31 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 32 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 33 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 34 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 35 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 36 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 37 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 38 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 39 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 43 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 44 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 45 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 46 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 47 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 48 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 51 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 52 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 53 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 54 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 55 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 56 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 59 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 60 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 61 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 62 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 70 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 73 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 77 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 78 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 79 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 80 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 81 | -------------------------------------------------------------------------------- /integration/cli_test.go: -------------------------------------------------------------------------------- 1 | // nolint: scopelint,gosec 2 | package integration 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "path/filepath" 12 | "reflect" 13 | "regexp" 14 | "runtime" 15 | "testing" 16 | ) 17 | 18 | var update = flag.Bool("update", false, "update golden files") 19 | 20 | var binaryName = "fixme" 21 | 22 | func fixturePath(t *testing.T, fixture string) string { 23 | _, filename, _, ok := runtime.Caller(0) 24 | if !ok { 25 | t.Fatalf("problems recovering caller information") 26 | } 27 | 28 | return filepath.Join(filepath.Dir(filename), fixture) 29 | } 30 | 31 | func writeFixture(t *testing.T, fixture string, content []byte) { 32 | err := ioutil.WriteFile(fixturePath(t, fixture), content, 0644) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func loadFixture(t *testing.T, fixture string) string { 39 | content, err := ioutil.ReadFile(fixturePath(t, fixture)) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | return cleanPath(string(content)) 45 | } 46 | 47 | // since the CI will likely have a different working path we need to massage the output a bit to remove the absolute path 48 | func cleanPath(input string) string { 49 | pathRE := regexp.MustCompile(`(?m)^.+testdata/test(-author)?\.txt`) 50 | substitution := "testdata/test.txt" 51 | 52 | return pathRE.ReplaceAllString(input, substitution) 53 | } 54 | 55 | func TestCliArgs(t *testing.T) { 56 | tests := []struct { 57 | name string 58 | args []string 59 | fixture string 60 | }{ 61 | {"test.txt", []string{"./testdata/test.txt"}, "test-txt.golden"}, 62 | {"test.txt JSON output", []string{"--json", "./testdata/test.txt"}, "test-txt-json.golden"}, 63 | {"test authors", []string{"./testdata/test-author.txt"}, "test-author-txt.golden"}, 64 | } 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | dir, err := os.Getwd() 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | cmd := exec.Command(path.Join(dir, "bin", binaryName), tt.args...) 74 | output, err := cmd.CombinedOutput() 75 | if err != nil { 76 | fmt.Printf("debug: dir: %s\n", dir) 77 | fmt.Printf("debug: cmd: %s\n", path.Join(dir, "bin", binaryName)) 78 | fmt.Printf("debug: args: %v\n", tt.args) 79 | fmt.Printf("debug: output: %s\n", output) 80 | fmt.Printf("debug: error: %s\n", err) 81 | t.Fatal(err) 82 | } 83 | 84 | if *update { 85 | writeFixture(t, tt.fixture, output) 86 | } 87 | 88 | actual := cleanPath(string(output)) 89 | 90 | expected := loadFixture(t, tt.fixture) 91 | 92 | if !reflect.DeepEqual(actual, expected) { 93 | t.Fatalf("actual = %s, expected = %s", actual, expected) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestMain(m *testing.M) { 100 | err := os.Chdir("..") 101 | if err != nil { 102 | fmt.Printf("could not change dir: %v", err) 103 | os.Exit(1) 104 | } 105 | makeCmd := exec.Command("make", "build") 106 | err = makeCmd.Run() 107 | if err != nil { 108 | fmt.Printf("could not make binary for %s: %v", binaryName, err) 109 | os.Exit(1) 110 | } 111 | 112 | os.Exit(m.Run()) 113 | } 114 | -------------------------------------------------------------------------------- /integration/test-author-txt.golden: -------------------------------------------------------------------------------- 1 | • testdata/test-author.txt [2 messages]: 2 | [Line 1] ✐ NOTE from Test Author: make sure author is correctly displayed 3 | [Line 2] ✐ NOTE from Test Author: make sure author is correctly displayed 4 | 5 | -------------------------------------------------------------------------------- /integration/test-txt-json.golden: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "testdata/test.txt", 3 | "matches": [ 4 | { 5 | "lineNumber": 1, 6 | "tag": "NOTE", 7 | "label": " ✐ NOTE", 8 | "author": "", 9 | "message": "test note " 10 | }, 11 | { 12 | "lineNumber": 2, 13 | "tag": "OPTIMIZE", 14 | "label": " ↻ OPTIMIZE", 15 | "author": "", 16 | "message": "test optimize" 17 | }, 18 | { 19 | "lineNumber": 3, 20 | "tag": "TODO", 21 | "label": " ✓ TODO", 22 | "author": "", 23 | "message": "test todo" 24 | }, 25 | { 26 | "lineNumber": 4, 27 | "tag": "HACK", 28 | "label": " ✄ HACK", 29 | "author": "", 30 | "message": "test hack" 31 | }, 32 | { 33 | "lineNumber": 5, 34 | "tag": "XXX", 35 | "label": " ✗ XXX", 36 | "author": "", 37 | "message": "test XXX" 38 | }, 39 | { 40 | "lineNumber": 6, 41 | "tag": "FIXME", 42 | "label": " ☠ FIXME", 43 | "author": "", 44 | "message": "test fixme" 45 | }, 46 | { 47 | "lineNumber": 7, 48 | "tag": "BUG", 49 | "label": "☢ BUG", 50 | "author": "", 51 | "message": "test bug" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /integration/test-txt.golden: -------------------------------------------------------------------------------- 1 | • testdata/test.txt [7 messages]: 2 | [Line 1] ✐ NOTE: test note 3 | [Line 2] ↻ OPTIMIZE: test optimize 4 | [Line 3] ✓ TODO: test todo 5 | [Line 4] ✄ HACK: test hack 6 | [Line 5] ✗ XXX: test XXX 7 | [Line 6] ☠ FIXME: test fixme 8 | [Line 7] ☢ BUG: test bug 9 | 10 | 11 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakewarren/fixme/130d262cb63dcb67b8675431ecf37d05839d360a/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/apex/log" 15 | "github.com/apex/log/handlers/cli" 16 | "github.com/cloudflare/ahocorasick" 17 | "github.com/fatih/color" 18 | "github.com/remeh/sizedwaitgroup" 19 | kingpin "gopkg.in/alecthomas/kingpin.v2" 20 | ) 21 | 22 | // a matcher contains the regex for comment tags we are looking for 23 | type matcher struct { 24 | Regex string 25 | Label string 26 | Tag string 27 | } 28 | 29 | // Result holds data for all matches found in a file 30 | type Result struct { 31 | Filename string `json:"filename"` 32 | Matches []match `json:"matches"` 33 | } 34 | 35 | // a match contains data for each comment tag found in a file 36 | type match struct { 37 | LineNumber int `json:"lineNumber"` 38 | Tag string `json:"tag"` 39 | Label string `json:"label"` 40 | Author string `json:"author"` 41 | Message string `json:"message"` 42 | } 43 | 44 | var ( 45 | version string 46 | skipHidden *bool 47 | jsonOutput *bool 48 | includeVendor *bool 49 | ignoreDirs *[]string 50 | ignoreExts *[]string 51 | lineLengthLimit *int 52 | 53 | outputMux sync.Mutex 54 | 55 | matchers []matcher 56 | tagMatcher *ahocorasick.Matcher 57 | ) 58 | 59 | func main() { 60 | app := kingpin.New("fixme", "Searches for comment tags in code") 61 | filePath := app.Arg("file", "the file or directory to scan (default=current directory)").String() 62 | jsonOutput = app.Flag("json", "output in JSON (default=false)").Short('j').Default("false").PlaceHolder("false").Bool() 63 | skipHidden = app.Flag("skip-hidden", "skip hidden folders (default=true)").Default("true").PlaceHolder("true").Bool() 64 | includeVendor = app.Flag("include-vendor", "include vendor directory (default=false)").Default("false").PlaceHolder("false").Bool() 65 | ignoreDirs = app.Flag("ignore-dir", "pattern of directories to ignore").Short('i').Default("vendor").Strings() 66 | ignoreExts = app.Flag("ignore-exts", "pattern of file extensions to ignore").PlaceHolder(".txt").Strings() 67 | lineLengthLimit = app.Flag("line-length-limit", "number of max characters in a line").Default("1000").Int() 68 | logLvl := app.Flag("log-level", "log level (debug|info|error)").Short('l').Default("error").Enum("debug", "info", "error") 69 | 70 | app.Version(version).VersionFlag.Short('V') 71 | app.HelpFlag.Short('h') 72 | app.UsageTemplate(kingpin.SeparateOptionalFlagsUsageTemplate) 73 | kingpin.MustParse(app.Parse(os.Args[1:])) 74 | 75 | log.SetHandler(cli.New(os.Stdout)) 76 | log.SetLevelFromString(*logLvl) 77 | 78 | // set up the regex values 79 | matchers = initMatchers() 80 | tagMatcher = ahocorasick.NewStringMatcher([]string{"NOTE", "OPTIMIZE", "TODO", "HACK", "XXX", "FIXME", "BUG"}) 81 | 82 | // if the user doesn't specify a directory assume the current directory 83 | if *filePath == "" { 84 | *filePath = "." 85 | } 86 | 87 | // since the vendor directory is ignored by default, 88 | // ensure we remove the vendor directory from the list of ignored directories if the user wants it included 89 | if *includeVendor { 90 | for i, v := range *ignoreDirs { 91 | if v == "vendor" { 92 | *ignoreDirs = append((*ignoreDirs)[:i], (*ignoreDirs)[i+1:]...) 93 | break 94 | } 95 | } 96 | } 97 | 98 | // get the files from the path the user specified 99 | cleanPath, err := filepath.Abs(*filePath) 100 | if err != nil { 101 | log.WithError(err).Fatal("Error identifying files") 102 | } 103 | 104 | fileList := getFiles(cleanPath) 105 | results := make([]Result, len(fileList)) 106 | 107 | wg := sizedwaitgroup.New(runtime.NumCPU()) 108 | for i, file := range fileList { 109 | wg.Add() 110 | go func(file string, i int) { 111 | defer wg.Done() 112 | 113 | // scan for comment tags within the file 114 | result, err := processFile(file) 115 | if err != nil { 116 | log.WithError(err).Errorf("Error processing %s", file) 117 | } 118 | 119 | results[i] = result 120 | }(file, i) 121 | } 122 | wg.Wait() 123 | 124 | if *jsonOutput { 125 | // acquire a lock to ensure our output is stable 126 | outputMux.Lock() 127 | defer outputMux.Unlock() 128 | 129 | for _, result := range results { 130 | if result.Matches == nil { 131 | continue 132 | } 133 | 134 | if b, marshalErr := json.MarshalIndent(result, "", " "); marshalErr == nil { 135 | fmt.Println(string(b)) 136 | } 137 | } 138 | 139 | } else { 140 | for _, result := range results { 141 | // print results from the file 142 | printMatches(result) 143 | } 144 | } 145 | } 146 | 147 | func printMatches(result Result) { 148 | // acquire a lock to ensure our output is stable 149 | outputMux.Lock() 150 | defer outputMux.Unlock() 151 | 152 | numOfMatches := len(result.Matches) 153 | if numOfMatches == 0 { 154 | return 155 | } 156 | 157 | color.Set(color.FgHiWhite, color.Bold) 158 | fmt.Printf("• %s ", result.Filename) 159 | color.Unset() 160 | 161 | color.Set(color.Faint) 162 | if numOfMatches == 1 { 163 | fmt.Printf("[%d message]:\n", numOfMatches) 164 | } else { 165 | fmt.Printf("[%d messages]:\n", numOfMatches) 166 | } 167 | color.Unset() 168 | 169 | for _, m := range result.Matches { 170 | color.Set(color.Faint) 171 | fmt.Printf(" [Line %d]\t", m.LineNumber) 172 | color.Unset() 173 | 174 | switch m.Tag { 175 | case "NOTE": 176 | color.Set(color.Bold, color.FgHiGreen) 177 | if len(m.Author) > 0 { 178 | fmt.Printf(" %s from %s:", m.Label, m.Author) 179 | } else { 180 | fmt.Printf(" %s:", m.Label) 181 | } 182 | color.Unset() 183 | color.Set(color.FgGreen) 184 | fmt.Printf(" %s\n", m.Message) 185 | color.Unset() 186 | case "OPTIMIZE": 187 | color.Set(color.Bold, color.FgHiBlue) 188 | if len(m.Author) > 0 { 189 | fmt.Printf(" %s from %s:", m.Label, m.Author) 190 | } else { 191 | fmt.Printf(" %s:", m.Label) 192 | } 193 | color.Unset() 194 | color.Set(color.FgBlue) 195 | fmt.Printf(" %s\n", m.Message) 196 | color.Unset() 197 | case "TODO": 198 | color.Set(color.Bold, color.FgHiMagenta) 199 | if len(m.Author) > 0 { 200 | fmt.Printf(" %s from %s:", m.Label, m.Author) 201 | } else { 202 | fmt.Printf(" %s:", m.Label) 203 | } 204 | color.Unset() 205 | color.Set(color.FgHiMagenta) 206 | fmt.Printf(" %s\n", m.Message) 207 | color.Unset() 208 | case "HACK": 209 | color.Set(color.Bold, color.FgHiYellow) 210 | if len(m.Author) > 0 { 211 | fmt.Printf(" %s from %s:", m.Label, m.Author) 212 | } else { 213 | fmt.Printf(" %s:", m.Label) 214 | } 215 | color.Unset() 216 | color.Set(color.FgYellow) 217 | fmt.Printf(" %s\n", m.Message) 218 | color.Unset() 219 | case "XXX": 220 | color.Set(color.Bold, color.FgHiCyan) 221 | if len(m.Author) > 0 { 222 | fmt.Printf(" %s from %s:", m.Label, m.Author) 223 | } else { 224 | fmt.Printf(" %s:", m.Label) 225 | } 226 | color.Unset() 227 | color.Set(color.FgCyan) 228 | fmt.Printf(" %s\n", m.Message) 229 | color.Unset() 230 | case "FIXME": 231 | color.Set(color.Bold, color.FgHiRed) 232 | if len(m.Author) > 0 { 233 | fmt.Printf(" %s from %s:", m.Label, m.Author) 234 | } else { 235 | fmt.Printf(" %s:", m.Label) 236 | } 237 | color.Unset() 238 | color.Set(color.FgRed) 239 | fmt.Printf(" %s\n", m.Message) 240 | color.Unset() 241 | case "BUG": 242 | fmt.Print(" ") 243 | color.Set(color.Bold, color.FgWhite, color.BgRed) 244 | if len(m.Author) > 0 { 245 | fmt.Printf("%s from %s:", m.Label, m.Author) 246 | } else { 247 | fmt.Printf("%s:", m.Label) 248 | } 249 | color.Unset() 250 | fmt.Print(" ") 251 | color.Set(color.FgRed) 252 | fmt.Printf("%s\n", m.Message) 253 | color.Unset() 254 | fmt.Println() 255 | } 256 | 257 | } 258 | fmt.Println() 259 | } 260 | 261 | func processFile(file string) (Result, error) { 262 | var result Result 263 | result.Filename = file 264 | 265 | log.Debug("Processing " + file) 266 | f, err := os.Open(file) 267 | if err != nil { 268 | return result, err 269 | } 270 | 271 | scanner := bufio.NewScanner(f) 272 | lineNumber := 0 273 | for scanner.Scan() { 274 | line := scanner.Text() 275 | lineNumber++ 276 | 277 | // skip if the line is too long 278 | if len(line) > *lineLengthLimit { 279 | continue 280 | } 281 | 282 | // check the line with the MPM before running against regular expressions 283 | hits := tagMatcher.Match([]byte(line)) 284 | if len(hits) == 0 { 285 | continue 286 | } 287 | 288 | // check the line against the regexes 289 | for _, m := range matchers { 290 | re := regexp.MustCompile(m.Regex) 291 | 292 | if re.MatchString(line) { 293 | // skip tags with an empty message 294 | if len(re.FindStringSubmatch(line)[2]) == 0 { 295 | continue 296 | } 297 | result.Matches = append(result.Matches, match{lineNumber, m.Tag, m.Label, re.FindStringSubmatch(line)[1], re.FindStringSubmatch(line)[2]}) 298 | } 299 | } 300 | } 301 | 302 | return result, nil 303 | } 304 | 305 | // getFiles returns a list of the files to be processed 306 | func getFiles(filePath string) []string { 307 | fileList := []string{} 308 | err := filepath.Walk(filePath, func(path string, f os.FileInfo, err error) error { 309 | // catch any errors while walking the filepath 310 | if err != nil { 311 | return err 312 | } 313 | 314 | // skip hidden directories if the user requests it 315 | if *skipHidden { 316 | if f.IsDir() && strings.HasPrefix(f.Name(), ".") { 317 | return filepath.SkipDir 318 | } 319 | } 320 | 321 | // skip any directories the user wants to ignore 322 | if len(*ignoreDirs) > 0 { 323 | for _, dir := range *ignoreDirs { 324 | if f.IsDir() && strings.HasPrefix(f.Name(), dir) { 325 | return filepath.SkipDir 326 | } 327 | } 328 | } 329 | 330 | // skip any files with an extension the user wants to ignore 331 | if len(*ignoreExts) > 0 { 332 | for _, ext := range *ignoreExts { 333 | if !f.IsDir() && strings.HasSuffix(f.Name(), ext) { 334 | return filepath.SkipDir 335 | } 336 | } 337 | } 338 | 339 | if !f.IsDir() { 340 | fileList = append(fileList, path) 341 | log.Debug("found file: " + path) 342 | } 343 | 344 | return nil 345 | }) 346 | if err != nil { 347 | log.WithError(err).Fatal("failed getting file names") 348 | } 349 | 350 | return fileList 351 | } 352 | 353 | func initMatchers() []matcher { 354 | return []matcher{ 355 | { 356 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*NOTE\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 357 | Label: ` ✐ NOTE`, 358 | Tag: "NOTE", 359 | }, 360 | { 361 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*OPTIMIZE\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 362 | Label: ` ↻ OPTIMIZE`, 363 | Tag: "OPTIMIZE", 364 | }, 365 | { 366 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*TODO\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 367 | Label: ` ✓ TODO`, 368 | Tag: "TODO", 369 | }, 370 | { 371 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*HACK\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 372 | Label: ` ✄ HACK`, 373 | Tag: "HACK", 374 | }, 375 | { 376 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*XXX\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 377 | Label: ` ✗ XXX`, 378 | Tag: "XXX", 379 | }, 380 | { 381 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*FIXME\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 382 | Label: ` ☠ FIXME`, 383 | Tag: "FIXME", 384 | }, 385 | { 386 | Regex: `(?i)(?:[\/\/][\/\*]|#|%)\s*BUG\b\s*(?:\(([^:]*)\))*\s*:?\s*(.*)`, 387 | Label: `☢ BUG`, 388 | Tag: "BUG", 389 | }, 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /testdata/test-author.txt: -------------------------------------------------------------------------------- 1 | #NOTE(Test Author): make sure author is correctly displayed 2 | //NOTE(Test Author): make sure author is correctly displayed -------------------------------------------------------------------------------- /testdata/test.txt: -------------------------------------------------------------------------------- 1 | //NOTE: test note 2 | #OPTIMIZE: test optimize 3 | //TODO: test todo 4 | //HACK: test hack 5 | //XXX: test XXX 6 | //FIXME: test fixme 7 | //BUG: test bug 8 | --------------------------------------------------------------------------------