diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml new file mode 100644 index 0000000..ddcffc2 --- /dev/null +++ b/.github/workflows/haskell-ci.yml @@ -0,0 +1,196 @@ +# This GitHub workflow config has been generated by a script via +# +# haskell-ci 'github' 'argon.cabal' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# version: 0.19.20250315 +# +# REGENDATA ("0.19.20250315",["github","argon.cabal"]) +# +name: Haskell-CI +on: + - push + - pull_request +jobs: + linux: + name: Haskell-CI - Linux - ${{ matrix.compiler }} + runs-on: ubuntu-24.04 + timeout-minutes: + 60 + container: + image: buildpack-deps:jammy + continue-on-error: ${{ matrix.allow-failure }} + strategy: + matrix: + include: + - compiler: ghc-9.10.1 + compilerKind: ghc + compilerVersion: 9.10.1 + setup-method: ghcup + allow-failure: false + fail-fast: false + steps: + - name: apt-get install + run: | + apt-get update + apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 + - name: Install GHCup + run: | + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.40.0/x86_64-linux-ghcup-0.1.40.0 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + - name: Install cabal-install + run: | + "$HOME/.ghcup/bin/ghcup" install cabal 3.12.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + echo "CABAL=$HOME/.ghcup/bin/cabal-3.12.1.0 -vnormal+nowrap" >> "$GITHUB_ENV" + - name: Install GHC (GHCup) + if: matrix.setup-method == 'ghcup' + run: | + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + HC=$("$HOME/.ghcup/bin/ghcup" whereis ghc "$HCVER") + HCPKG=$(echo "$HC" | sed 's#ghc$#ghc-pkg#') + HADDOCK=$(echo "$HC" | sed 's#ghc$#haddock#') + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HCPKG" >> "$GITHUB_ENV" + echo "HADDOCK=$HADDOCK" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: Set PATH and environment variables + run: | + echo "$HOME/.cabal/bin" >> $GITHUB_PATH + echo "LANG=C.UTF-8" >> "$GITHUB_ENV" + echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" + echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" + HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') + echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" + echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" + echo "HEADHACKAGE=false" >> "$GITHUB_ENV" + echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: env + run: | + env + - name: write cabal config + run: | + mkdir -p $CABAL_DIR + cat >> $CABAL_CONFIG <> $CABAL_CONFIG < cabal-plan.xz + echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c - + xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan + rm -f cabal-plan.xz + chmod a+x $HOME/.cabal/bin/cabal-plan + cabal-plan --version + - name: checkout + uses: actions/checkout@v4 + with: + path: source + - name: initial cabal.project for sdist + run: | + touch cabal.project + echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project + cat cabal.project + - name: sdist + run: | + mkdir -p sdist + $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist + - name: unpack + run: | + mkdir -p unpacked + find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; + - name: generate cabal.project + run: | + PKGDIR_argon="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/argon-[0-9.]*')" + echo "PKGDIR_argon=${PKGDIR_argon}" >> "$GITHUB_ENV" + rm -f cabal.project cabal.project.local + touch cabal.project + touch cabal.project.local + echo "packages: ${PKGDIR_argon}" >> cabal.project + echo "package argon" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project + cat >> cabal.project <> cabal.project.local + cat cabal.project + cat cabal.project.local + - name: dump install plan + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all + cabal-plan + - name: restore cache + uses: actions/cache/restore@v4 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store + restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- + - name: install dependencies + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all + - name: build w/o tests + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: build + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always + - name: tests + run: | + $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + - name: cabal check + run: | + cd ${PKGDIR_argon} || false + ${CABAL} -vnormal check + - name: haddock + run: | + $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + - name: unconstrained build + run: | + rm -f cabal.project.local + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: save cache + if: always() + uses: actions/cache/save@v4 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store diff --git a/.gitignore b/.gitignore index 38eeae3..7a9c6d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sw[eop] dist/ +dist-newstyle/ cabal-dev *.o *.hi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 85e9a79..0000000 --- a/.travis.yml +++ /dev/null @@ -1,149 +0,0 @@ -# Use new container infrastructure to enable caching -sudo: false - -# Do not choose a language; we provide our own build tools. -language: generic - -# Caching so the next build will be fast too. -cache: - directories: - - $HOME/.ghc - - $HOME/.cabal - - $HOME/.stack - -matrix: - include: - - env: BUILD=cabal GHCVER=8.0.2 CABALVER=1.24 HAPPYVER=1.19.5 ALEXVER=3.1.7 - compiler: ": #GHC 8.0.2" - addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.2,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}} - - env: BUILD=cabal GHCVER=8.2.2 CABALVER=2.0 HAPPYVER=1.19.5 ALEXVER=3.1.7 - compiler: ": #GHC 8.2.2" - addons: {apt: {packages: [cabal-install-2.0,ghc-8.2.2,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}} - # Build with the newest GHC and cabal-install. This is an accepted failure, - # see below. - - env: BUILD=cabal GHCVER=head CABALVER=head HAPPYVER=1.19.5 ALEXVER=3.1.7 - compiler: ": #GHC HEAD" - addons: {apt: {packages: [cabal-install-head,ghc-head,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}} - # The Stack builds. We can pass in arbitrary Stack arguments via the ARGS - # variable, such as using --stack-yaml to point to a different file. - - env: BUILD=stack ARGS="" - compiler: ": #stack default" - addons: {apt: {packages: [libgmp-dev]}} - - env: BUILD=stack ARGS="--resolver lts-9" - compiler: ": #stack 8.0.2" - addons: {apt: {packages: [libgmp-dev]}} - - env: BUILD=stack ARGS="--resolver lts-11" - compiler: ": #stack 8.2.2" - addons: {apt: {packages: [libgmp-dev]}} - # Nightly builds are allowed to fail - - env: BUILD=stack ARGS="--resolver nightly" - compiler: ": #stack nightly" - addons: {apt: {packages: [libgmp-dev]}} - - # Build on macOS in addition to Linux - - env: BUILD=stack ARGS="" - compiler: ": #stack default osx" - os: osx - - - env: BUILD=stack ARGS="--resolver lts-9" - compiler: ": #stack 8.0.2 osx" - os: osx - - - env: BUILD=stack ARGS="--resolver lts-11" - compiler: ": #stack 8.2.2 osx" - os: osx - - allow_failures: - - env: BUILD=cabal GHCVER=head CABALVER=head HAPPYVER=1.19.5 ALEXVER=3.1.7 - - env: BUILD=cabal GHCVER=8.2.2 CABALVER=2.0 HAPPYVER=1.19.5 ALEXVER=3.1.7 # TODO: I need to figure out why this one fails with /home/travis/.cabal/logs/ghc-8.2.2/integer-logarithms-1.0.2.1-2a6l3Ge7uAMKtkov70g6We.log: openFile: does not exist (No such file or directory) - - env: BUILD=stack ARGS="--resolver nightly" - # TODO: on MacOS it the following builds will fail with `error: non-portable path to file '".stack-work/dist/x86_64-osx/Cabal-2.0.1.0/build/Argon/autogen/cabal_macros.h"';` - - os: osx - -before_install: -# Using compiler above sets CC to an invalid value, so unset it -- unset CC - -# We want to always allow newer versions of packages when building on GHC HEAD -- CABALARGS="" -- if [ "x$GHCVER" = "xhead" ]; then CABALARGS=--allow-newer; fi - -# Download and unpack the stack executable -- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$HOME/.local/bin:/opt/alex/$ALEXVER/bin:/opt/happy/$HAPPYVER/bin:$HOME/.cabal/bin:$PATH -- mkdir -p ~/.local/bin -- | - if [ `uname` = "Darwin" ] - then - travis_retry curl --insecure -L https://www.stackage.org/stack/osx-x86_64 | tar xz --strip-components=1 --include '*/stack' -C ~/.local/bin - else - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' - fi - - # Use the more reliable S3 mirror of Hackage - mkdir -p $HOME/.cabal - echo 'remote-repo: hackage.haskell.org:http://hackage.fpcomplete.com/' > $HOME/.cabal/config - echo 'remote-repo-cache: $HOME/.cabal/packages' >> $HOME/.cabal/config - - if [ "$CABALVER" != "1.16" ] - then - echo 'jobs: $ncpus' >> $HOME/.cabal/config - fi - -install: -- echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]" -- if [ -f configure.ac ]; then autoreconf -i; fi -- | - set -ex - case "$BUILD" in - stack) - # Add in extra-deps for older snapshots, as necessary - stack --no-terminal --install-ghc $ARGS test --bench --dry-run || ( \ - stack --no-terminal $ARGS build cabal-install && \ - stack --no-terminal $ARGS solver --update-config) - - # Build the dependencies - stack --no-terminal --install-ghc $ARGS test --bench --only-dependencies - ;; - cabal) - cabal --version - travis_retry cabal update - - # Get the list of packages from the stack.yaml file. Note that - # this will also implicitly run hpack as necessary to generate - # the .cabal files needed by cabal-install. - PACKAGES=$(stack --install-ghc query locals | grep '^ *path' | sed 's@^ *path:@@') - - cabal install --only-dependencies --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES - ;; - esac - set +ex - -script: -- | - set -ex - case "$BUILD" in - stack) - stack --no-terminal $ARGS test --bench --no-run-benchmarks --haddock --no-haddock-deps --coverage --pedantic - ;; - cabal) - cabal install --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES - - ORIGDIR=$(pwd) - for dir in $PACKAGES - do - cd $dir - cabal check || [ "$CABALVER" == "1.16" ] - cabal sdist - PKGVER=$(cabal info . | awk '{print $2;exit}') - SRC_TGZ=$PKGVER.tar.gz - cd dist - tar zxfv "$SRC_TGZ" - cd "$PKGVER" - cabal configure --enable-tests --ghc-options -O0 - cabal build - cabal test - cd $ORIGDIR - done - ;; - esac - set +ex diff --git a/README.md b/README.md index 6472c08..0c521fb 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,6 @@ -

- - Argon - -

- -

- - Tests - - - Code coverage - - - License - - - Version - -

- -

- Argon measures your code's cyclomatic complexity. -

- -

- Argon screenshot -

- -
- -### Installing - -Simple as ``stack install argon`` or ``cabal install argon``. -Note: if you are using Stack and your resolver if too old, you might have to -add some packages to your `stack.yaml` file. - -#### GHC compatibility - -Argon is compatible with GHC version 8.0.2 and above. In the -[releases](https://github.com/rubik/argon/releases) page you can find binaries -for older versions of `argon` which support GHC versions 7.8 and 7.10. +# Argon + +Argon measures your code's cyclomatic complexity. ### About the complexity being measured @@ -93,28 +49,35 @@ potential maintainability issues. The Argon executable expects a list of file paths (files or directories): - $ argon --no-color --min 2 src - src/Argon/Types.hs - 61:5 toJSON - 2 - src/Argon/Visitor.hs - 55:1 visitExp - 5 - 62:1 visitOp - 4 - 28:11 visit - 2 - 35:1 getFuncName - 2 - src/Argon/Parser.hs - 55:1 parseModuleWithCpp - 3 - 88:1 customLogAction - 3 - 35:1 analyze - 2 - 39:9 analysis - 2 - src/Argon/Formatters.hs - 61:1 formatResult - 3 - 42:1 coloredFunc - 2 - 43:11 color - 2 - src/Argon/Results.hs - 35:1 export - 3 - 28:1 filterResults - 2 - src/Argon/Loc.hs - 18:11 toRealSrcLoc - 2 +```bash +$ argon --no-color --min 2 src +src/Argon/Formatters.hs + 42:1 coloredFunc - 2 + 44:5 color - 2 + 63:1 formatResult - 2 +src/Argon/Types.hs + 98:3 toJSON - 2 +src/Argon/Cabal.hs + 24:5 toString - 3 +src/Argon/Parser.hs + 68:1 parseModuleWithCpp - 5 + 37:1 analyze - 2 + 44:7 analysis - 2 +src/Argon/Walker.hs + 12:1 allFiles - 2 +src/Argon/Results.hs + 35:1 filterNulls - 3 + 56:1 exportStream - 3 + 46:1 filterResults - 2 +src/Argon/Loc.hs + 19:5 toRealSrcLoc - 2 +src/Argon/Visitor.hs + 67:1 visitExp - 6 + 75:1 visitOp - 4 + 35:5 visit - 2 +src/Argon/SYB/Utils.hs + 21:1 everythingStaged - 2 +``` For every file, Argon sorts results with the following criteria (and in this order): @@ -136,57 +99,32 @@ complexity score: #### JSON Results can also be exported to JSON: -```json -$ argon --json --min 2 src +```bash +$ argon --json --min 5 src | jq [ - { "blocks": [ ], "path": "src/Argon.hs", "type": "result" }, - { - "blocks": [{ "complexity": 2, "name": "toJSON", "lineno": 61, "col": 5 }], - "path": "src/Argon/Types.hs", - "type": "result" - }, - { - "blocks": [ - { "complexity": 5, "name": "visitExp", "lineno": 55, "col": 1 }, - { "complexity": 4, "name": "visitOp", "lineno": 62, "col": 1 }, - { "complexity": 2, "name": "visit", "lineno": 28, "col": 11 }, - { "complexity": 2, "name": "getFuncName", "lineno": 35, "col": 1 } - ], - "path": "src/Argon/Visitor.hs", - "type": "result" - }, { "blocks": [ - { "complexity": 3, "name": "parseModuleWithCpp", "lineno": 55, "col": 1 }, - { "complexity": 3, "name": "customLogAction", "lineno": 88, "col": 1 }, - { "complexity": 2, "name": "analyze", "lineno": 35, "col": 1 }, - { "complexity": 2, "name": "analysis", "lineno": 39, "col": 9 } + { + "col": 1, + "complexity": 5, + "lineno": 68, + "name": "parseModuleWithCpp" + } ], "path": "src/Argon/Parser.hs", "type": "result" }, { "blocks": [ - { "complexity": 3, "name": "formatResult", "lineno": 61, "col": 1 }, - { "complexity": 2, "name": "coloredFunc", "lineno": 42, "col": 1 }, - { "complexity": 2, "name": "color", "lineno": 43, "col": 11 } + { + "col": 1, + "complexity": 6, + "lineno": 67, + "name": "visitExp" + } ], - "path": "src/Argon/Formatters.hs", - "type": "result" - }, - { - "blocks": [ - { "complexity": 3, "name": "export", "lineno": 35, "col": 1 }, - { "complexity": 2, "name": "filterResults", "lineno": 28, "col": 1 } - ], - "path": "src/Argon/Results.hs", - "type": "result" - }, - { - "blocks": [{ "complexity": 2, "name": "toRealSrcLoc", "lineno": 18, "col": 11 }], - "path": "src/Argon/Loc.hs", + "path": "src/Argon/Visitor.hs", "type": "result" - }, - { "blocks": [ ], "path": "src/Argon/Preprocess.hs", "type": "result" } + } ] ``` diff --git a/Setup.hs b/Setup.hs index 9a994af..e8ef27d 100644 --- a/Setup.hs +++ b/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/USAGE.txt b/USAGE.txt deleted file mode 100644 index 4ab2bf9..0000000 --- a/USAGE.txt +++ /dev/null @@ -1,11 +0,0 @@ -Usage: - argon [options] ... - -Options: - -h --help show this help - -m --min= the minimum complexity to show in results - --cabal-file= path to Cabal main file - --cabal-macros= Cabal header file with versions macros - -I --include-dir= additional directory with header files - --no-color results are not colored - -j --json results are serialized to JSON diff --git a/app/Main.hs b/app/Main.hs index 6384dcd..9a91d00 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,49 +1,95 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE QuasiQuotes #-} module Main where -import Pipes -import Pipes.Safe (runSafeT) -import qualified Pipes.Prelude as P import Control.Monad (forM_) -import System.Environment (getArgs) -import System.Console.Docopt +import Options.Applicative qualified as Opt import Argon +import Control.Applicative ((<|>)) +import Data.List.NonEmpty (NonEmpty) +import Data.Maybe (maybeToList) +import Options.Applicative ((<**>)) +import Options.Applicative.NonEmpty qualified as Opt +parseArgon :: Opt.Parser Argon +parseArgon = Argon <$> parseConfig <*> parsePaths + where + parseConfig :: Opt.Parser Config + parseConfig = Config <$> minComplexity <*> exts <*> headers <*> includeDirs <*> outputMode + where + minComplexity = + Opt.option + Opt.auto + ( Opt.long "min" + <> Opt.short 'm' + <> Opt.metavar "N" + <> Opt.value 1 + <> Opt.showDefault + <> Opt.help "the minimum complexity to show in results" + ) -patterns :: Docopt -patterns = [docoptFile|USAGE.txt|] + -- HACK: this should rather be parsed into a separate type, and + -- constructed from there. For now we overload the use of the field + -- since the types are (coincidentally) the same + exts = + fmap maybeToList . Opt.optional $ + Opt.strOption + ( Opt.long "cabal-file" + <> Opt.metavar "PATH" + <> Opt.help "path to Cabal main file" + ) -getArgOrExit :: Arguments -> Option -> IO String -getArgOrExit = getArgOrExitWith patterns + headers = + Opt.many $ + Opt.strOption + ( Opt.long "cabal-macros" + <> Opt.metavar "PATH" + <> Opt.help "Cabal header file with versions macros" + ) -getOpt :: Arguments -> String -> String -> String -getOpt args def opt = getArgWithDefault args def $ longOption opt + includeDirs = + Opt.many $ + Opt.strOption + ( Opt.long "include-dir" + <> Opt.short 'I' + <> Opt.metavar "PATH" + <> Opt.help "additional directory with header files" + ) -readConfig :: Arguments -> IO Config -readConfig args = do - xFlags <- maybe (return []) parseExts $ getArg args $ longOption "cabal-file" - return Config { - minCC = read $ getOpt args "1" "min" - , exts = xFlags - , headers = args `getAllArgs` longOption "cabal-macros" - , includeDirs = args `getAllArgs` longOption "include-dir" - , outputMode = if args `isPresent` longOption "json" - then JSON - else if args `isPresent` longOption "no-color" - then BareText - else Colored - } + outputMode = json <|> noColor + where + json = + Opt.flag' + JSON + ( Opt.long "json" + <> Opt.short 'j' + <> Opt.help "results are serialized to JSON" + ) + noColor = + Opt.flag + Colored + BareText + ( Opt.long "no-color" + <> Opt.help "results are not colored" + ) + + parsePaths :: Opt.Parser (NonEmpty FilePath) + parsePaths = Opt.some1 path + + path :: Opt.Parser FilePath + path = Opt.argument Opt.str (Opt.metavar "FILE") main :: IO () main = do - args <- parseArgsOrExit patterns =<< getArgs - let ins = args `getAllArgs` argument "paths" - conf <- readConfig args - forM_ ins $ \path -> do - let source = allFiles path - >-> P.mapM (liftIO . analyze conf) - >-> P.map (filterResults conf) - >-> P.filter filterNulls - runSafeT $ runEffect $ exportStream conf source + argon' <- Opt.execParser opts + exts <- concat <$> traverse parseExts argon'.config.exts + let argon = argon' {config = argon'.config {exts = exts}} + forM_ argon.paths $ \path -> do + sourceFiles <- allFiles path + analysisResults <- traverse (analyze argon.config) sourceFiles + let filteredResults = filter filterNulls . map (filterResults argon.config) $ analysisResults + exportStream argon.config filteredResults + where + opts = + Opt.info + (parseArgon <**> Opt.helper) + Opt.fullDesc diff --git a/argon.cabal b/argon.cabal index 022e174..c77c27d 100644 --- a/argon.cabal +++ b/argon.cabal @@ -1,3 +1,4 @@ +cabal-version: 3.0 name: argon version: 0.4.1.0 synopsis: Measure your code's complexity @@ -10,115 +11,114 @@ maintainer: michelelacchia@gmail.com copyright: 2015 Michele Lacchia category: Development, Static Analysis build-type: Simple -cabal-version: >=1.18 +tested-with: GHC ==9.10.1 description: - Argon performs static analysis on your code in order to compute cyclomatic - complexity. It is a quantitative measure of the number of linearly - indipendent paths through the code. - . - The intended usage is through Argon's executable, which accepts a list of - files or directories to analyze. The data can be optionally exported to - JSON. + Argon performs static analysis on your code in order to compute cyclomatic + complexity. It is a quantitative measure of the number of linearly + indipendent paths through the code. + . + The intended usage is through Argon's executable, which accepts a list of + files or directories to analyze. The data can be optionally exported to + JSON. + extra-source-files: - stack.yaml - README.md - CHANGELOG.md - USAGE.txt - test/data/*.hs - test/data/*.h - test/data/*.cabal - test/data/include/*.h - test/tree/*.hs - test/tree/*.txt - test/tree/sub/*.hs - test/tree/sub2/*.hs -tested-with: GHC >= 8.0.2 && < 9 + CHANGELOG.md + README.md + stack.yaml + test/data/*.cabal + test/data/*.h + test/data/*.hs + test/data/include/*.h + test/tree/*.hs + test/tree/*.txt + test/tree/sub/*.hs + test/tree/sub2/*.hs + +common common-extensions + default-extensions: + DataKinds + DerivingStrategies + DuplicateRecordFields + LambdaCase + OverloadedRecordDot + + default-language: GHC2021 + +common common-warnings + ghc-options: + -Wall -Wcompat -Widentities -Wpartial-fields + -Wredundant-constraints -fhide-source-paths -fshow-hole-constraints + -Wunused-packages library - hs-source-dirs: src - exposed-modules: Argon - other-modules: Argon.Parser - Argon.Visitor - Argon.Results - Argon.Formatters - Argon.Types - Argon.Preprocess - Argon.Loc - Argon.Cabal - Argon.SYB.Utils - Argon.Walker - build-depends: base >=4.7 && <5 - , ansi-terminal - , aeson - , bytestring - , pipes - , pipes-group - , pipes-safe - , pipes-bytestring - , lens-simple - , ghc - , ghc-boot - , ghc-paths - , ghc-syb-utils - , syb - , Cabal - , containers - , directory - , system-filepath - , dirstream - , filepath - default-language: Haskell2010 - ghc-options: -Wall - -Wcompat - -Wincomplete-record-updates - -Wincomplete-uni-patterns - -Wredundant-constraints - if impl(ghc < 7.8) - buildable: False + import: + common-warnings + , common-extensions + + hs-source-dirs: src + exposed-modules: Argon + other-modules: + Argon.Cabal + Argon.Formatters + Argon.Loc + Argon.Parser + Argon.Preprocess + Argon.Results + Argon.SYB.Utils + Argon.Types + Argon.Visitor + Argon.Walker + + build-depends: + , aeson + , ansi-terminal + , base >=4.7 && <5 + , bytestring + , Cabal + , cpphs + , directory + , ghc-lib-parser + , ghc-lib-parser-ex + , Glob + , syb + + ghc-options: -threaded executable argon - hs-source-dirs: app - main-is: Main.hs - ghc-options: -Wall - build-depends: base >=4.7 && <5 - , argon -any - , docopt >=0.7 - , pipes >=4.1 - , pipes-safe >=2.2 - default-language: Haskell2010 - if impl(ghc < 7.8) - buildable: False + import: + common-warnings + , common-extensions + + hs-source-dirs: app + main-is: Main.hs + build-depends: + , argon + , base >=4.7 && <5 + , optparse-applicative + + ghc-options: -threaded -rtsopts -with-rtsopts=-N test-suite argon-test - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Spec.hs - other-modules: ArgonSpec - build-depends: base >=4.7 && <5 - , argon - , ansi-terminal - , ghc - , aeson - , hspec - , QuickCheck - , filepath - , pipes - , pipes-safe - ghc-options: -Wall - -threaded -rtsopts -with-rtsopts=-N - - default-language: Haskell2010 - if impl(ghc < 7.8) - buildable: False + import: + common-warnings + , common-extensions + + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + other-modules: ArgonSpec + build-depends: + , aeson + , ansi-terminal + , argon + , base >=4.7 && <5 + , filepath + , ghc-lib-parser + , hspec + , QuickCheck -test-suite style - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: HLint.hs - build-depends: base - , hlint - default-language: Haskell2010 - ghc-options: -Wall + build-tool-depends: hspec-discover:hspec-discover >=2 && <3 + ghc-options: -threaded -rtsopts -with-rtsopts=-N source-repository head type: git diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..422ef84 --- /dev/null +++ b/cabal.project @@ -0,0 +1,3 @@ +packages: . + +with-compiler: ghc-9.10.1 diff --git a/cabal.project.freeze b/cabal.project.freeze new file mode 100644 index 0000000..01ab46e --- /dev/null +++ b/cabal.project.freeze @@ -0,0 +1,91 @@ +active-repositories: hackage.haskell.org:merge +constraints: any.Cabal ==3.12.0.0, + any.Cabal-syntax ==3.12.0.0, + any.Glob ==0.10.2, + any.HUnit ==1.6.2.0, + any.OneTuple ==0.4.2, + any.QuickCheck ==2.14.3, + any.StateVar ==1.2.2, + any.aeson ==2.2.3.0, + any.ansi-terminal ==1.0.2, + any.ansi-terminal-types ==0.11.5, + any.array ==0.5.7.0, + any.assoc ==1.1.1, + any.base ==4.20.0.0, + any.base-orphans ==0.9.2, + any.bifunctors ==5.6.2, + any.binary ==0.8.9.2, + any.bytestring ==0.12.1.0, + any.call-stack ==0.4.0, + any.character-ps ==0.1, + any.colour ==2.3.6, + any.comonad ==5.0.8, + any.containers ==0.7, + any.contravariant ==1.5.5, + any.cpphs ==1.20.9.1, + any.data-fix ==0.3.4, + any.deepseq ==1.5.0.0, + any.directory ==1.3.8.3, + any.distributive ==0.6.2.1, + any.dlist ==1.0, + any.exceptions ==0.10.7, + any.filepath ==1.5.2.0, + any.generically ==0.1.1, + any.ghc-bignum ==1.3, + any.ghc-boot-th ==9.10.1, + any.ghc-internal ==9.1001.0, + any.ghc-lib-parser ==9.10.1.20240511, + any.ghc-lib-parser-ex ==9.10.0.0, + any.ghc-prim ==0.11.0, + any.hashable ==1.4.7.0, + any.haskell-lexer ==1.1.1, + any.hspec ==2.11.9, + any.hspec-core ==2.11.9, + any.hspec-discover ==2.11.9, + any.hspec-expectations ==0.8.4, + any.indexed-traversable ==0.1.4, + any.indexed-traversable-instances ==0.1.2, + any.integer-conversion ==0.1.1, + any.integer-logarithms ==1.0.3.1, + any.mtl ==2.3.1, + any.network-uri ==2.6.4.2, + any.optparse-applicative ==0.18.1.0, + any.os-string ==2.0.2, + any.parsec ==3.1.17.0, + any.polyparse ==1.13, + any.pretty ==1.1.3.6, + any.prettyprinter ==1.7.1, + any.prettyprinter-ansi-terminal ==1.1.3, + any.primitive ==0.9.0.0, + any.process ==1.6.19.0, + any.quickcheck-io ==0.2.0, + any.random ==1.2.1.2, + any.rts ==1.0.2, + any.scientific ==0.3.8.0, + any.semialign ==1.3.1, + any.semigroupoids ==6.0.1, + any.splitmix ==0.1.0.5, + any.stm ==2.5.3.1, + any.strict ==0.5, + any.syb ==0.7.2.4, + any.tagged ==0.8.8, + any.template-haskell ==2.22.0.0, + any.text ==2.1.1, + any.text-iso8601 ==0.1.1, + any.text-short ==0.1.6, + any.tf-random ==0.5, + any.th-abstraction ==0.7.0.0, + any.th-compat ==0.1.5, + any.these ==1.2.1, + any.time ==1.12.2, + any.time-compat ==1.9.6.1, + any.transformers ==0.6.1.1, + any.transformers-compat ==0.7.2, + any.uniplate ==1.6.13, + any.unix ==2.8.5.1, + any.unordered-containers ==0.2.20, + any.uuid-types ==1.0.6, + any.vector ==0.13.1.0, + any.vector-stream ==0.1.0.1, + any.witherable ==0.4.2 +index-state: hackage.haskell.org 2024-10-02T20:12:10Z diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..614db46 --- /dev/null +++ b/flake.lock @@ -0,0 +1,58 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1726153070, + "narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1727716680, + "narHash": "sha256-uMVkVHL4r3QmlZ1JM+UoJwxqa46cgHnIfqGzVlw5ca4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b5b22b42c0d10c7d2463e90a546c394711e3a724", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1725233747, + "narHash": "sha256-Ss8QWLXdr2JCBPcYChJhz4xJm+h/xjl4G0c0XlP6a74=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5bccb16 --- /dev/null +++ b/flake.nix @@ -0,0 +1,75 @@ +{ + description = "argon"; + + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} { + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + perSystem = { + pkgs, + system, + ... + }: let + pkgs = inputs.nixpkgs.legacyPackages.${system}; + pkgsStatic = inputs.nixpkgs.legacyPackages.${system}.pkgsMusl; + myOverlay = pkgs.haskell.lib.compose.packageSourceOverrides { + argon = ./.; + }; + hsPkgs = pkgs.haskell.packages.ghc910.extend myOverlay; + + fixGhc = pkg: + pkg.override { + enableRelocatedStaticLibs = true; + enableShared = false; + enableDwarf = false; + }; + + hsPkgsStatic = + (pkgsStatic.haskell.packages.ghc910.override (old: { + ghc = fixGhc old.ghc; + buildHaskellPackages = old.buildHaskellPackages.override (oldBHP: { + ghc = fixGhc oldBHP.ghc; + }); + })) + .extend + myOverlay; + in { + devShells.default = hsPkgs.shellFor { + packages = p: [p.argon]; + nativeBuildInputs = [ + hsPkgs.cabal-install + hsPkgs.haskell-language-server + hsPkgs.fourmolu + hsPkgs.cabal-fmt + ]; + }; + + packages.default = pkgs.haskell.lib.dontCheck hsPkgs.argon; + + packages.static = pkgs.haskell.lib.overrideCabal hsPkgsStatic.argon (old: { + configureFlags = + (old.configureFlags or []) + ++ [ + "--ghc-option=-optl=-static" + "--extra-lib-dirs=${pkgsStatic.gmp6.override {withStatic = true;}}/lib" + "--extra-lib-dirs=${pkgsStatic.libffi.overrideAttrs (old: {dontDisableStatic = true;})}/lib" + "--extra-lib-dirs=${pkgsStatic.zlib.static}/lib" + ]; + enableSharedExecutables = false; + enableSharedLibraries = false; + doCheck = false; + }); + + formatter = pkgs.alejandra; + }; + }; +} diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..0979e2d --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,50 @@ +# Number of spaces per indentation step +indentation: 2 + +# Max line length for automatic line breaking +column-limit: none + +# Styling of arrows in type signatures (choices: trailing, leading, or leading-args) +function-arrows: leading + +# How to place commas in multi-line lists, records, etc. (choices: leading or trailing) +comma-style: leading + +# Styling of import/export lists (choices: leading, trailing, or diff-friendly) +import-export-style: leading + +# Whether to full-indent or half-indent 'where' bindings past the preceding body +indent-wheres: true + +# Whether to leave a space before an opening record brace +record-brace-space: true + +# Number of spaces between top-level declarations +newlines-between-decls: 1 + +# How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact) +haddock-style: single-line + +# How to print module docstring +haddock-style-module: null + +# Styling of let blocks (choices: auto, inline, newline, or mixed) +let-style: auto + +# How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space) +in-style: right-align + +# Whether to put parentheses around a single constraint (choices: auto, always, or never) +single-constraint-parens: never + +# Output Unicode syntax (choices: detect, always, or never) +unicode: never + +# Give the programmer more choice on where to insert blank lines +respectful: true + +# Fixity information for operators +fixities: [] + +# Module reexports Fourmolu should know about +reexports: [] diff --git a/src/Argon.hs b/src/Argon.hs index f81d456..94da2c8 100644 --- a/src/Argon.hs +++ b/src/Argon.hs @@ -8,39 +8,44 @@ -- -- Programmatic interface to Argon. module Argon - ( - -- * Types - AnalysisResult - , ComplexityBlock(CC) - , OutputMode(..) - , Config(..) - , defaultConfig - , Loc - , LModule + ( -- * Types + AnalysisResult + , ComplexityBlock (CC) + , OutputMode (..) + , Config (..) + , defaultConfig + , Argon (..) + , Loc + , LModule + -- * Gathering source files - , allFiles + , allFiles + -- * Parsing - , analyze - , parseModule - , parseExts + , analyze + , parseModule + , parseExts + -- * Manipulating results - , order - , filterResults - , filterNulls - , exportStream + , order + , filterResults + , filterNulls + , exportStream + -- * Formatting results - , bareTextFormatter - , coloredTextFormatter + , bareTextFormatter + , coloredTextFormatter + -- * Utilities - , srcSpanToLoc - , locToString - , tagMsg - ) where + , srcSpanToLoc + , locToString + , tagMsg + ) where -import Argon.Parser (LModule, analyze, parseModule) -import Argon.Results (order, filterResults, filterNulls, exportStream) import Argon.Cabal (parseExts) -import Argon.Types +import Argon.Formatters (bareTextFormatter, coloredTextFormatter) import Argon.Loc +import Argon.Parser (LModule, analyze, parseModule) +import Argon.Results (exportStream, filterNulls, filterResults, order) +import Argon.Types import Argon.Walker (allFiles) -import Argon.Formatters (bareTextFormatter, coloredTextFormatter) diff --git a/src/Argon/Cabal.hs b/src/Argon/Cabal.hs index ef3c9d8..55a4fa3 100644 --- a/src/Argon/Cabal.hs +++ b/src/Argon/Cabal.hs @@ -1,34 +1,30 @@ {-# LANGUAGE CPP #-} -module Argon.Cabal (parseExts) - where -import Data.List (nub) -#if __GLASGOW_HASKELL__ < 710 -import Control.Applicative ((<$>)) -#endif +module Argon.Cabal (parseExts) +where -import qualified Distribution.PackageDescription as Dist -import qualified Distribution.PackageDescription.Parse as Dist -import qualified Distribution.Verbosity as Dist -import qualified Language.Haskell.Extension as Dist +import Data.List (nub) +import Distribution.PackageDescription qualified as Dist +import Distribution.Simple.PackageDescription qualified as Dist +import Distribution.Verbosity qualified as Dist +import Language.Haskell.Extension qualified as Dist -- | Parse the given Cabal file generate a list of GHC extension flags. The -- extension names are read from the default-extensions field in the library -- section. parseExts :: FilePath -> IO [String] -#if __GLASGOW_HASKELL__ < 802 -parseExts path = extract <$> Dist.readPackageDescription Dist.silent path -#else parseExts path = extract <$> Dist.readGenericPackageDescription Dist.silent path -#endif - where extract pkg = maybe [] extFromBI $ - Dist.libBuildInfo . Dist.condTreeData <$> Dist.condLibrary pkg + where + extract pkg = maybe [] (extFromBI . Dist.libBuildInfo . Dist.condTreeData) (Dist.condLibrary pkg) extFromBI :: Dist.BuildInfo -> [String] extFromBI binfo = map toString . nub $ allExts - where toString (Dist.UnknownExtension ext) = ext - toString (Dist.EnableExtension ext) = show ext - toString (Dist.DisableExtension ext) = show ext - allExts = concatMap ($ binfo) - [Dist.defaultExtensions, Dist.otherExtensions, Dist.oldExtensions] + where + toString (Dist.UnknownExtension ext) = ext + toString (Dist.EnableExtension ext) = show ext + toString (Dist.DisableExtension ext) = show ext + allExts = + concatMap + ($ binfo) + [Dist.defaultExtensions, Dist.otherExtensions, Dist.oldExtensions] diff --git a/src/Argon/Formatters.hs b/src/Argon/Formatters.hs index d96ebf2..fcad003 100644 --- a/src/Argon/Formatters.hs +++ b/src/Argon/Formatters.hs @@ -1,30 +1,30 @@ -{-# LANGUAGE LambdaCase #-} -module Argon.Formatters (bareTextFormatter, coloredTextFormatter) - where +module Argon.Formatters (bareTextFormatter, coloredTextFormatter) where -import Text.Printf (printf) import System.Console.ANSI +import Text.Printf (printf) -import Pipes -import qualified Pipes.Prelude as P - -import Argon.Types import Argon.Loc +import Argon.Types - -bareTextFormatter :: MonadIO m => Pipe (FilePath, AnalysisResult) String m () -bareTextFormatter = formatResult +bareTextFormatter :: [(FilePath, AnalysisResult)] -> [String] +bareTextFormatter = + formatResult id ("\terror: " ++) (\(CC (l, func, cc)) -> printf "\t%s %s - %d" (locToString l) func cc) -coloredTextFormatter :: MonadIO m => Pipe (FilePath, AnalysisResult) String m () -coloredTextFormatter = formatResult +coloredTextFormatter :: [(FilePath, AnalysisResult)] -> [String] +coloredTextFormatter = + formatResult (\name -> bold ++ name ++ reset) (printf "\t%serror%s: %s" (fore Red) reset) - (\(CC (l, func, cc)) -> printf "\t%s %s - %s" (locToString l) - (coloredFunc func l) - (coloredRank cc)) + ( \(CC (l, func, cc)) -> + printf + "\t%s %s - %s" + (locToString l) + (coloredFunc func l) + (coloredRank cc) + ) -- | ANSI bold color bold :: String @@ -40,24 +40,26 @@ reset = setSGRCode [] coloredFunc :: String -> Loc -> String coloredFunc f (_, c) = fore color ++ f ++ reset - where color = if c == 1 then Cyan else Magenta + where + color = if c == 1 then Cyan else Magenta coloredRank :: Int -> String coloredRank c = printf "%s%s (%d)%s" (fore color) rank c reset - where (color, rank) - | c <= 5 = (Green, "A") - | c <= 10 = (Yellow, "B") - | otherwise = (Red, "C") - -formatResult :: (MonadIO m) - => (String -> String) -- ^ The header formatter - -> (String -> String) -- ^ The error formatter - -> (ComplexityBlock -> String) -- ^ The single line formatter - -> Pipe (FilePath, AnalysisResult) String m () -formatResult header errorF singleF = for cat $ \case - (path, Left err) -> do - yield $ header path - yield $ errorF err - (path, Right rs) -> do - yield $ header path - each rs >-> P.map singleF + where + (color, rank) + | c <= 5 = (Green, "A") + | c <= 10 = (Yellow, "B") + | otherwise = (Red, "C") + +formatResult + :: (String -> String) + -- ^ The header formatter + -> (String -> String) + -- ^ The error formatter + -> (ComplexityBlock -> String) + -- ^ The single line formatter + -> [(FilePath, AnalysisResult)] + -> [String] +formatResult header errorF singleF = concatMap $ \case + (path, Left err) -> [header path, errorF err] + (path, Right rs) -> header path : (map singleF rs) diff --git a/src/Argon/Loc.hs b/src/Argon/Loc.hs index dc75576..3a2493e 100644 --- a/src/Argon/Loc.hs +++ b/src/Argon/Loc.hs @@ -1,24 +1,23 @@ module Argon.Loc (Loc, srcSpanToLoc, locToString, tagMsg) - where +where -import Text.Printf (printf) import Control.Arrow ((&&&)) - -import qualified SrcLoc as GHC -import qualified FastString as GHC +import GHC.Data.FastString qualified as GHC +import GHC.Types.SrcLoc qualified as GHC +import Text.Printf (printf) -- | Type synonym representing a location in the source code. The tuple -- represents the following: @(start line, start col)@. type Loc = (Int, Int) - -- | Convert a GHC's 'SrcSpan' to a @(line, column)@ pair. In case of a GHC's -- "bad span" the resulting pair is @(0, 0)@. srcSpanToLoc :: GHC.SrcSpan -> Loc srcSpanToLoc ss = lloc $ GHC.srcSpanStart ss - where lloc = (GHC.srcLocLine &&& GHC.srcLocCol) . toRealSrcLoc - toRealSrcLoc (GHC.RealSrcLoc z) = z - toRealSrcLoc _ = GHC.mkRealSrcLoc (GHC.mkFastString "no info") 0 0 + where + lloc = (GHC.srcLocLine &&& GHC.srcLocCol) . toRealSrcLoc + toRealSrcLoc (GHC.RealSrcLoc z _) = z + toRealSrcLoc _ = GHC.mkRealSrcLoc (GHC.mkFastString "no info") 0 0 -- | Convert a location to a string of the form "line:col" locToString :: Loc -> String diff --git a/src/Argon/Parser.hs b/src/Argon/Parser.hs index 0405929..7490f48 100644 --- a/src/Argon/Parser.hs +++ b/src/Argon/Parser.hs @@ -1,46 +1,50 @@ {-# LANGUAGE CPP #-} + module Argon.Parser (LModule, analyze, parseModule) - where +where -import Control.Monad (void) -import qualified Control.Exception as E +import Control.Exception qualified as E -import qualified GHC hiding (parseModule) -import qualified SrcLoc as GHC -import qualified Lexer as GHC -import qualified Parser as GHC -import qualified DynFlags as GHC -import qualified GHC.LanguageExtensions as GHC -import qualified HeaderInfo as GHC -import qualified MonadUtils as GHC -import qualified Outputable as GHC -import qualified FastString as GHC -import qualified StringBuffer as GHC -import GHC.Paths (libdir) +import GHC.LanguageExtensions qualified as GHC +import Argon.Loc import Argon.Preprocess -import Argon.Visitor (funcsCC) import Argon.Types -import Argon.Loc +import Argon.Visitor (funcsCC) +import Data.Maybe (mapMaybe) +import GHC.Driver.DynFlags qualified as GHC +import GHC.Driver.Ppr qualified as GHC +import GHC.Hs.Extension qualified as GHC +import GHC.Parser.Lexer qualified as GHC +import GHC.Types.SrcLoc qualified as GHC +import GHC.Utils.Outputable qualified as GHC +import Language.Haskell.GhclibParserEx.GHC.Driver.Session qualified as GhclibParserEx +import Language.Haskell.GhclibParserEx.GHC.Parser qualified as GHC.Parser +import Language.Haskell.Syntax qualified as GHC -- | Type synonym for a syntax node representing a module tagged with a -- 'SrcSpan' -type LModule = GHC.Located (GHC.HsModule GHC.RdrName) - +type LModule = GHC.Located (GHC.HsModule GHC.GhcPs) -- | Parse the code in the given filename and compute cyclomatic complexity for -- every function binding. -analyze :: Config -- ^ Configuration options - -> FilePath -- ^ The filename corresponding to the source code - -> IO (FilePath, AnalysisResult) +analyze + :: Config + -- ^ Configuration options + -> FilePath + -- ^ The filename corresponding to the source code + -> IO (FilePath, AnalysisResult) analyze conf file = do - parseResult <- (do + parseResult <- + ( do result <- parseModule conf file - E.evaluate result) `E.catch` handleExc - let analysis = case parseResult of - Left err -> Left err - Right ast -> Right $ funcsCC ast - return (file, analysis) + E.evaluate result + ) + `E.catch` handleExc + let analysis = case parseResult of + Left err -> Left err + Right ast -> Right $ funcsCC ast + return (file, analysis) handleExc :: E.SomeException -> IO (Either String LModule) handleExc = return . Left . show @@ -48,57 +52,42 @@ handleExc = return . Left . show -- | Parse a module with the default instructions for the C pre-processor -- Only the includes directory is taken from the config parseModule :: Config -> FilePath -> IO (Either String LModule) -parseModule conf = parseModuleWithCpp conf $ - defaultCppOptions { cppInclude = includeDirs conf - , cppFile = headers conf - } +parseModule conf = + parseModuleWithCpp conf $ + defaultCppOptions + { cppInclude = includeDirs conf + , cppFile = headers conf + } -- | Parse a module with specific instructions for the C pre-processor. -parseModuleWithCpp :: Config - -> CppOptions - -> FilePath - -> IO (Either String LModule) -parseModuleWithCpp conf cppOptions file = - GHC.runGhc (Just libdir) $ do - dflags <- initDynFlags conf file +parseModuleWithCpp + :: Config + -> CppOptions + -> FilePath + -> IO (Either String LModule) +parseModuleWithCpp conf cppOptions file = do + str <- readFile file + eDflags <- setExtensions (mapMaybe GhclibParserEx.readExtension conf.exts, []) file str + case eDflags of + Left err -> error err + Right dflags -> do let useCpp = GHC.xopt GHC.Cpp dflags - (fileContents, dflags1) <- + str' <- if useCpp - then getPreprocessedSrcDirect cppOptions file - else do - contents <- GHC.liftIO $ readFile file - return (contents, dflags) - return $ - case parseCode dflags1 file fileContents of - GHC.PFailed ss m -> Left $ tagMsg (srcSpanToLoc ss) - (GHC.showSDoc dflags m) - GHC.POk _ pmod -> Right pmod + then getPreprocessedSrc cppOptions file str + else pure str + eDflags' <- setExtensions (mapMaybe GhclibParserEx.readExtension conf.exts, []) file str' + case eDflags' of + Left err -> error err + Right dflags' -> do + return $ + case parseCode dflags' file str' of + GHC.PFailed ps -> + Left $ + tagMsg + (srcSpanToLoc $ GHC.mkSrcSpanPs (GHC.last_loc ps)) + (GHC.showSDoc dflags (GHC.ppr $ GHC.getPsMessages ps)) + GHC.POk _ pmod -> Right pmod parseCode :: GHC.DynFlags -> FilePath -> String -> GHC.ParseResult LModule -parseCode = runParser GHC.parseModule - -runParser :: GHC.P a -> GHC.DynFlags -> FilePath -> String -> GHC.ParseResult a -runParser parser flags filename str = GHC.unP parser parseState - where location = GHC.mkRealSrcLoc (GHC.mkFastString filename) 1 1 - buffer = GHC.stringToStringBuffer str - parseState = GHC.mkPState flags buffer location - -initDynFlags :: GHC.GhcMonad m => Config -> FilePath -> m GHC.DynFlags -initDynFlags conf file = do - dflags0 <- GHC.getSessionDynFlags - (dflags1,_,_) <- GHC.parseDynamicFlagsCmdLine dflags0 - [GHC.L GHC.noSrcSpan ("-X" ++ e) | e <- exts conf] - src_opts <- GHC.liftIO $ GHC.getOptionsFromFile dflags1 file - (dflags2, _, _) <- GHC.parseDynamicFilePragma dflags1 src_opts - let dflags3 = dflags2 { GHC.log_action = customLogAction } - void $ GHC.setSessionDynFlags dflags3 - return dflags3 - -customLogAction :: GHC.LogAction -customLogAction dflags _ severity srcSpan _ m = - case severity of - GHC.SevFatal -> throwError - GHC.SevError -> throwError - _ -> return () - where throwError = E.throwIO $ GhcParseError (srcSpanToLoc srcSpan) - (GHC.showSDoc dflags m) +parseCode flags file = GHC.Parser.parseFile file flags diff --git a/src/Argon/Preprocess.hs b/src/Argon/Preprocess.hs index 94fc262..5d814e6 100644 --- a/src/Argon/Preprocess.hs +++ b/src/Argon/Preprocess.hs @@ -3,58 +3,44 @@ -- seemed excessive. {-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} + -- | This module provides support for CPP and interpreter directives. module Argon.Preprocess - ( - CppOptions(..) - , defaultCppOptions - , getPreprocessedSrcDirect - ) where - -#if __GLASGOW_HASKELL__ < 710 -import Control.Applicative ((<$>)) -#endif -import qualified GHC -import qualified DynFlags as GHC -import qualified MonadUtils as GHC -import qualified DriverPhases as GHC -import qualified DriverPipeline as GHC -import qualified HscTypes as GHC + ( CppOptions (..) + , defaultCppOptions + , getPreprocessedSrc + , setExtensions + ) where + +import GHC.Driver.DynFlags qualified as GHC +import GHC.LanguageExtensions.Type qualified as GHC +import Language.Haskell.GhclibParserEx.GHC.Driver.Session (parsePragmasIntoDynFlags) +import Language.Haskell.GhclibParserEx.GHC.Settings.Config (fakeSettings) +import Language.Preprocessor.Cpphs qualified as Cpphs data CppOptions = CppOptions - { cppDefine :: [String] -- ^ CPP #define macros - , cppInclude :: [FilePath] -- ^ CPP Includes directory - , cppFile :: [FilePath] -- ^ CPP pre-include file - } - + { cppDefine :: [String] + -- ^ CPP #define macros + , cppInclude :: [FilePath] + -- ^ CPP Includes directory + , cppFile :: [FilePath] + -- ^ CPP pre-include file + } defaultCppOptions :: CppOptions defaultCppOptions = CppOptions [] [] [] -getPreprocessedSrcDirect :: (GHC.GhcMonad m) - => CppOptions - -> FilePath - -> m (String, GHC.DynFlags) -getPreprocessedSrcDirect cppOptions file = do - hscEnv <- GHC.getSession - let dfs = GHC.hsc_dflags hscEnv - newEnv = hscEnv { GHC.hsc_dflags = injectCppOptions cppOptions dfs } - (dflags', hspp_fn) <- - GHC.liftIO $ GHC.preprocess newEnv (file, Just (GHC.Cpp GHC.HsSrcFile)) - txt <- GHC.liftIO $ readFile hspp_fn - return (txt, dflags') - -injectCppOptions :: CppOptions -> GHC.DynFlags -> GHC.DynFlags -injectCppOptions CppOptions{..} dflags = - foldr addOptP dflags (map mkDefine cppDefine ++ map mkIncludeDir cppInclude - ++ map mkInclude cppFile) +setExtensions :: ([GHC.Extension], [GHC.Extension]) -> FilePath -> String -> IO (Either String GHC.DynFlags) +setExtensions = parsePragmasIntoDynFlags baseDynFlags where - mkDefine = ("-D" ++) - mkIncludeDir = ("-I" ++) - mkInclude = ("-include" ++) - -addOptP :: String -> GHC.DynFlags -> GHC.DynFlags -addOptP f = alterSettings (\s -> s { GHC.sOpt_P = f : GHC.sOpt_P s}) + baseDynFlags = GHC.defaultDynFlags fakeSettings -alterSettings :: (GHC.Settings -> GHC.Settings) -> GHC.DynFlags -> GHC.DynFlags -alterSettings f dflags = dflags { GHC.settings = f (GHC.settings dflags) } +getPreprocessedSrc :: CppOptions -> FilePath -> String -> IO String +getPreprocessedSrc opts = Cpphs.runCpphs opts' + where + opts' :: Cpphs.CpphsOptions + opts' = + Cpphs.defaultCpphsOptions + { Cpphs.includes = opts.cppInclude + -- , Cpphs.defines = opts.cppDefine + } diff --git a/src/Argon/Results.hs b/src/Argon/Results.hs index 279cdce..36f1da6 100644 --- a/src/Argon/Results.hs +++ b/src/Argon/Results.hs @@ -1,25 +1,20 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} + module Argon.Results (order, filterResults, filterNulls, exportStream) - where +where -import Data.Ord (comparing) import Data.List (sortBy) -import Data.String (IsString) +import Data.Ord (comparing) #if __GLASGOW_HASKELL__ < 710 import Control.Applicative ((<*), (*>)) #endif -import Data.Aeson (encode) -import Pipes -import Pipes.Group -import qualified Pipes.Prelude as P -import qualified Pipes.ByteString as PB -import Lens.Simple ((^.)) +import Data.Aeson qualified as Aeson import Argon.Formatters import Argon.Types - +import Data.ByteString.Lazy.Char8 qualified as BSL -- sortOn is built-in only in base 4.8.0.0 onwards sortOn :: Ord b => (a -> b) -> [a] -> [a] @@ -38,37 +33,28 @@ order = sortOn (\(CC ((l, _), f, cc)) -> (-cc, l, f)) -- are no blocks to show filterNulls :: (FilePath, AnalysisResult) -> Bool filterNulls (_, r) = case r of - Left _ -> True - Right [] -> False - _ -> True + Left _ -> True + Right [] -> False + _ -> True -- | Filter the results of the analysis, with respect to the given -- 'Config'. -filterResults :: Config - -> (FilePath, AnalysisResult) - -> (FilePath, AnalysisResult) +filterResults + :: Config + -> (FilePath, AnalysisResult) + -> (FilePath, AnalysisResult) filterResults _ (s, Left err) = (s, Left err) filterResults o (s, Right rs) = - (s, Right $ order [r | r@(CC (_, _, cc)) <- rs, cc >= minCC o]) + (s, Right $ order [r | r@(CC (_, _, cc)) <- rs, cc >= minCC o]) -- | Export analysis' results. How to export the data is defined by the -- 'Config' parameter. -exportStream :: (MonadIO m) - => Config - -> Producer (FilePath, AnalysisResult) m () - -> Effect m () +exportStream + :: Config + -> [(FilePath, AnalysisResult)] + -> IO () exportStream conf source = - case outputMode conf of - BareText -> source >-> bareTextFormatter >-> P.stdoutLn - Colored -> source >-> coloredTextFormatter >-> P.stdoutLn - JSON -> jsonStream (source >-> P.map encode) - >-> for cat (\i -> PB.fromLazy i >-> PB.stdout) - -jsonStream :: (MonadIO m) - => IsString a - => Producer a m () - -> Producer a m () -jsonStream source = yield "[" *> intersperse' "," source <* yield "]\n" - -intersperse' :: Monad m => a -> Producer a m r -> Producer a m r -intersperse' a producer = intercalates (yield a) (producer ^. chunksOf 1) + case outputMode conf of + BareText -> putStrLn . unlines $ bareTextFormatter source + Colored -> putStrLn . unlines $ coloredTextFormatter source + JSON -> BSL.putStrLn $ Aeson.encode source diff --git a/src/Argon/SYB/Utils.hs b/src/Argon/SYB/Utils.hs index a225a99..79b3110 100644 --- a/src/Argon/SYB/Utils.hs +++ b/src/Argon/SYB/Utils.hs @@ -1,18 +1,14 @@ -- The following code is temporarily taken from @alanz's fork of -- nominolo/ghc-syb. Argon will use the original ghc-syb when a new version -- is released on Hackage with @alanz's fixes. -{-# LANGUAGE CPP #-} {-# LANGUAGE RankNTypes #-} -module Argon.SYB.Utils (Stage(..), everythingStaged) - where -import GHC -import NameSet (NameSet) -import Data.Generics -#if __GLASGOW_HASKELL__ <= 708 -import Coercion -#endif +module Argon.SYB.Utils (Stage (..), everythingStaged) +where +import Data.Generics +import GHC.Types.Fixity qualified as GHC +import GHC.Types.Name.Set (NameSet) -- | Ghc Ast types tend to have undefined holes, to be filled -- by later compiler phases. We tag Asts with their source, @@ -23,20 +19,13 @@ data Stage = Parser | Renamer | TypeChecker deriving (Eq, Ord, Show) -- generated the Ast. everythingStaged :: Stage -> (r -> r -> r) -> r -> GenericQ r -> GenericQ r everythingStaged stage k z f x - | (const False -#if __GLASGOW_HASKELL__ <= 708 - `extQ` postTcType - `extQ` nameList - `extQ` coercion - `extQ` cmdTable -#endif - `extQ` fixity `extQ` nameSet) x = z + | ( const False + `extQ` fixity + `extQ` nameSet + ) + x = + z | otherwise = foldl k (f x) (gmapQ (everythingStaged stage k z f) x) - where nameSet = const (stage `elem` [Parser,TypeChecker]) :: NameSet -> Bool -#if __GLASGOW_HASKELL__ <= 708 - postTcType = const (stage < TypeChecker) :: PostTcType -> Bool - nameList = const (stage < TypeChecker) :: [Name] -> Bool - coercion = const (stage < TypeChecker) :: Coercion -> Bool - cmdTable = const (stage < TypeChecker) :: CmdSyntaxTable RdrName -> Bool -#endif - fixity = const (stage < Renamer) :: GHC.Fixity -> Bool + where + nameSet = const (stage `elem` [Parser, TypeChecker]) :: NameSet -> Bool + fixity = const (stage < Renamer) :: GHC.Fixity -> Bool diff --git a/src/Argon/Types.hs b/src/Argon/Types.hs index d430923..f0e16d8 100644 --- a/src/Argon/Types.hs +++ b/src/Argon/Types.hs @@ -1,89 +1,109 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} -#if __GLASGOW_HASKELL__ < 710 -{-# LANGUAGE DeriveDataTypeable #-} -#endif -module Argon.Types (ComplexityBlock(CC), AnalysisResult, Config(..) - , OutputMode(..), GhcParseError(..), defaultConfig) - where +module Argon.Types + ( ComplexityBlock (CC) + , AnalysisResult + , Config (..) + , OutputMode (..) + , GhcParseError (..) + , defaultConfig + , Argon (..) + ) +where -import Data.List (intercalate) +import Control.Exception (Exception) import Data.Aeson +import Data.List (intercalate) import Data.Typeable -import Control.Exception (Exception) import Argon.Loc +import Data.List.NonEmpty (NonEmpty) - -data GhcParseError = GhcParseError { - loc :: Loc +data GhcParseError = GhcParseError + { loc :: Loc , msg :: String -} deriving (Typeable) + } + deriving (Typeable) -- | Hold the data associated to a function binding: -- @(location, function name, complexity)@. newtype ComplexityBlock = CC (Loc, String, Int) - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord) -- | Represent the result of the analysis of one file. -- It can either be an error message or a list of -- 'ComplexityBlock's. type AnalysisResult = Either String [ComplexityBlock] +data Argon = Argon + { config :: Config + , paths :: NonEmpty FilePath + } + -- | Type holding all the options passed from the command line. -data Config = Config { - -- | Minimum complexity a block has to have to be shown in results. - minCC :: Int - -- | Extension to activate - , exts :: [String] - -- | Header files to be automatically included before preprocessing - , headers :: [FilePath] - -- | Additional include directories for the C preprocessor +data Config = Config + { minCC :: Int + -- ^ Minimum complexity a block has to have to be shown in results. + , exts :: [String] + -- ^ Extension to activate + , headers :: [FilePath] + -- ^ Header files to be automatically included before preprocessing , includeDirs :: [FilePath] - -- | Describe how the results should be exported. - , outputMode :: OutputMode + -- ^ Additional include directories for the C preprocessor + , outputMode :: OutputMode + -- ^ Describe how the results should be exported. } -- | Type describing how the results should be exported. -data OutputMode = BareText -- ^ Text-only output, no colors. - | Colored -- ^ Text-only output, with colors. - | JSON -- ^ Data is serialized to JSON. - deriving (Show, Eq) +data OutputMode + = -- | Text-only output, no colors. + BareText + | -- | Text-only output, with colors. + Colored + | -- | Data is serialized to JSON. + JSON + deriving (Show, Eq) -- | Default configuration options. -- -- __Warning__: These are not Argon's default options. defaultConfig :: Config -defaultConfig = Config { minCC = 1 - , exts = [] - , headers = [] - , includeDirs = [] - , outputMode = JSON - } +defaultConfig = + Config + { minCC = 1 + , exts = [] + , headers = [] + , includeDirs = [] + , outputMode = JSON + } instance Exception GhcParseError instance Show GhcParseError where - show e = tagMsg (loc e) $ fixNewlines (msg e) - where fixNewlines = intercalate "\n\t\t" . lines + show e = tagMsg (loc e) $ fixNewlines (msg e) + where + fixNewlines = intercalate "\n\t\t" . lines instance ToJSON ComplexityBlock where - toJSON (CC ((s, c), func, cc)) = - object [ "lineno" .= s - , "col" .= c - , "name" .= func - , "complexity" .= cc - ] + toJSON (CC ((s, c), func, cc)) = + object + [ "lineno" .= s + , "col" .= c + , "name" .= func + , "complexity" .= cc + ] instance {-# OVERLAPPING #-} ToJSON (FilePath, AnalysisResult) where - toJSON (p, Left err) = object [ "path" .= p - , "type" .= ("error" :: String) - , "message" .= err - ] - toJSON (p, Right rs) = object [ "path" .= p - , "type" .= ("result" :: String) - , "blocks" .= rs - ] + toJSON (p, Left err) = + object + [ "path" .= p + , "type" .= ("error" :: String) + , "message" .= err + ] + toJSON (p, Right rs) = + object + [ "path" .= p + , "type" .= ("result" :: String) + , "blocks" .= rs + ] diff --git a/src/Argon/Visitor.hs b/src/Argon/Visitor.hs index 2d625bc..68310a9 100644 --- a/src/Argon/Visitor.hs +++ b/src/Argon/Visitor.hs @@ -1,54 +1,60 @@ -{-# LANGUAGE CPP #-} -module Argon.Visitor (funcsCC) - where +{-# LANGUAGE DataKinds #-} -import Argon.SYB.Utils (Stage (..), everythingStaged) -import Control.Arrow ((&&&)) -import Data.Generics (Data, mkQ) +module Argon.Visitor (funcsCC) +where -import qualified GHC -import qualified OccName as GHC -import qualified RdrName as GHC +import Argon.SYB.Utils (Stage (..), everythingStaged) +import Control.Arrow ((&&&)) +import Data.Generics (Data, mkQ) -import Argon.Loc -import Argon.Types (ComplexityBlock (..)) +import GHC.Types.Name qualified as GHC -type Exp = GHC.HsExpr GHC.RdrName -type Function = GHC.HsBindLR GHC.RdrName GHC.RdrName -type MatchBody = GHC.LHsExpr GHC.RdrName +import Argon.Loc +import Argon.Types (ComplexityBlock (..)) +import GHC.Hs.Extension qualified as GHC +import GHC.Hs.Instances () +import GHC.Parser.Annotation qualified as GHC +import GHC.Types.Name.Reader qualified as GHC +import GHC.Types.SrcLoc qualified as GHC +import Language.Haskell.Syntax qualified as GHC +type Exp = GHC.HsExpr GHC.GhcPs +type Function = GHC.HsBind GHC.GhcPs +type MatchBody = GHC.LHsExpr GHC.GhcPs -- | Compute cyclomatic complexity of every function binding in the given AST. -funcsCC :: (Data from) => from -> [ComplexityBlock] +funcsCC :: Data from => from -> [ComplexityBlock] funcsCC = map funCC . getBinds funCC :: Function -> ComplexityBlock -funCC f = CC (getLocation $ GHC.fun_id f, getFuncName f, complexity f) +funCC f = CC (srcSpanToLoc $ GHC.getLocA $ GHC.fun_id f, getFuncName f, complexity f) -getBinds :: (Data from) => from -> [Function] +getBinds :: Data from => from -> [Function] getBinds = everythingStaged Parser (++) [] $ mkQ [] visit - where visit fun@GHC.FunBind {} = [fun] - visit _ = [] + where + visit fun@GHC.FunBind {} = [fun] + visit _ = [] -getLocation :: GHC.Located a -> Loc -getLocation = srcSpanToLoc . GHC.getLoc +-- getLocation :: GHC.Located a -> Loc +-- getLocation = srcSpanToLoc . GHC.getLoc getFuncName :: Function -> String getFuncName = getName . GHC.unLoc . GHC.fun_id complexity :: Function -> Int -complexity f = let matches = getMatches f - query = everythingStaged Parser (+) 0 $ 0 `mkQ` visit - visit = uncurry (+) . (visitExp &&& visitOp) - in length matches + sumWith getGRHSsFromMatch matches + sumWith query matches +complexity f = + let matches = getMatches f + query = everythingStaged Parser (+) 0 $ 0 `mkQ` visit + visit = uncurry (+) . (visitExp &&& visitOp) + in length matches + sumWith getGRHSsFromMatch matches + sumWith query matches -getMatches :: Function -> [GHC.LMatch GHC.RdrName MatchBody] +getMatches :: Function -> [GHC.LMatch GHC.GhcPs MatchBody] getMatches = GHC.unLoc . GHC.mg_alts . GHC.fun_matches -getGRHSsFromMatch :: GHC.LMatch GHC.RdrName MatchBody -> Int +getGRHSsFromMatch :: GHC.LMatch GHC.GhcPs MatchBody -> Int getGRHSsFromMatch match = length (getGRHSs' match) - 1 where - getGRHSs' :: GHC.LMatch GHC.RdrName MatchBody -> [GHC.LGRHS GHC.RdrName MatchBody] + getGRHSs' :: GHC.LMatch GHC.GhcPs MatchBody -> [GHC.LGRHS GHC.GhcPs MatchBody] getGRHSs' = GHC.grhssGRHSs . GHC.m_grhss . GHC.unLoc getName :: GHC.RdrName -> String @@ -58,21 +64,17 @@ sumWith :: (a -> Int) -> [a] -> Int sumWith f = sum . map f visitExp :: Exp -> Int -visitExp GHC.HsIf {} = 1 +visitExp GHC.HsIf {} = 1 visitExp (GHC.HsMultiIf _ alts) = length alts - 1 -#if __GLASGOW_HASKELL__ < 802 -visitExp (GHC.HsCase _ alts) = length (GHC.unLoc . GHC.mg_alts $ alts) - 1 -visitExp (GHC.HsLamCase _ alts) = length (GHC.unLoc . GHC.mg_alts $ alts) - 1 -#else -visitExp (GHC.HsLamCase mg) = length (GHC.unLoc . GHC.mg_alts $ mg) - 1 -visitExp (GHC.HsCase _ mg) = length (GHC.unLoc . GHC.mg_alts $ mg) - 1 -#endif -visitExp _ = 0 +visitExp (GHC.HsLam _ GHC.LamCase mg) = length (GHC.unLoc . GHC.mg_alts $ mg) - 1 +visitExp (GHC.HsLam _ GHC.LamCases mg) = length (GHC.unLoc . GHC.mg_alts $ mg) - 1 +visitExp (GHC.HsCase _ _ mg) = length (GHC.unLoc . GHC.mg_alts $ mg) - 1 +visitExp _ = 0 visitOp :: Exp -> Int -visitOp (GHC.OpApp _ (GHC.L _ (GHC.HsVar op)) _ _) = - case getName (GHC.unLoc op) of - "||" -> 1 - "&&" -> 1 - _ -> 0 +visitOp (GHC.OpApp _ _ (GHC.L _ (GHC.HsVar _ op)) _) = + case getName (GHC.unLoc op) of + "||" -> 1 + "&&" -> 1 + _ -> 0 visitOp _ = 0 diff --git a/src/Argon/Walker.hs b/src/Argon/Walker.hs index b31c6ca..b959279 100644 --- a/src/Argon/Walker.hs +++ b/src/Argon/Walker.hs @@ -1,34 +1,21 @@ {-# LANGUAGE OverloadedStrings #-} -module Argon.Walker (allFiles) - where -import Data.DirStream (childOf) -import Data.List (isSuffixOf) -import Filesystem.Path.CurrentOS (decodeString, encodeString) -import Pipes (ListT, MonadIO, Producer, each, - every, liftIO, (>->)) -import qualified Pipes.Prelude as P -import Pipes.Safe -import System.Directory (doesDirectoryExist, doesFileExist, - pathIsSymbolicLink) -import System.FilePath (takeExtension) +module Argon.Walker (allFiles) where + +import Control.Monad (guard) +import System.Directory (doesFileExist) +import System.FilePath.Glob qualified as Glob -- | Starting from a path, generate a sequence of paths corresponding -- to Haskell files. The filesystem is traversed depth-first. -allFiles :: (MonadIO m, MonadSafe m) => FilePath -> Producer FilePath m () +allFiles :: FilePath -> IO [FilePath] allFiles path = do - isFile <- liftIO $ doesFileExist path - if isFile then each [path] >-> P.filter (".hs" `isSuffixOf`) - else every $ hsFilesIn path + isFile <- doesFileExist path + if isFile + then do + guard $ Glob.match "*.hs" path + pure [path] + else hsFilesIn path --- | List the regular files in a directory. -hsFilesIn :: MonadSafe m => FilePath -> ListT m FilePath -hsFilesIn path = do - child <- encodeString <$> childOf (decodeString path) - isDir <- liftIO $ doesDirectoryExist child - isSymLink <- liftIO $ pathIsSymbolicLink child - if isDir && not isSymLink - then hsFilesIn child - else if not isSymLink && takeExtension child == ".hs" - then return child - else mempty +hsFilesIn :: FilePath -> IO [FilePath] +hsFilesIn path = Glob.globDir1 (Glob.compile "**/*.hs") path diff --git a/stack-travis-coveralls.yaml b/stack-travis-coveralls.yaml deleted file mode 100644 index 4b79afd..0000000 --- a/stack-travis-coveralls.yaml +++ /dev/null @@ -1,8 +0,0 @@ -flags: {} -extra-package-dbs: [] -packages: -- '.' -extra-deps: -- stack-hpc-coveralls-0.0.0.3 -- docopt-0.7.0.4 -resolver: lts-3.11 diff --git a/stack.yaml b/stack.yaml index 6b097a3..48dfb97 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,4 +1,3 @@ -resolver: lts-11.6 - -extra-deps: [dirstream-1.0.3] - +# NOTE: as of 2024-10-12, there is no resolver with GHC 9.10 yet, +# so the stack build is broken for the time being. +resolver: nightly-2024-10-11 diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..aa89ab6 --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: [] +snapshots: +- completed: + sha256: 1435a8c3c419d01af402eef9c0938adb601031d8ed625622ef1519fa726db042 + size: 665058 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2024/10/11.yaml + original: nightly-2024-10-11 diff --git a/test/ArgonSpec.hs b/test/ArgonSpec.hs index cd29d49..cd8481f 100644 --- a/test/ArgonSpec.hs +++ b/test/ArgonSpec.hs @@ -1,42 +1,50 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module ArgonSpec (spec) - where +where -import Data.Aeson (encode) -import Data.List (sort) -import GHC.Stack (HasCallStack) -import Text.Printf (printf) -#if __GLASGOW_HASKELL__ < 710 -import Control.Applicative ((<$>), (<*>)) -#endif -import qualified FastString as GHC -import Pipes (Producer, (>->), each) -import qualified SrcLoc as GHC -import System.Console.ANSI (Color (..), ConsoleIntensity(BoldIntensity), setSGRCode, - SGR(SetColor, SetConsoleIntensity), - ConsoleLayer(Foreground), ColorIntensity(Dull)) -import System.FilePath (()) -import System.IO.Unsafe (unsafePerformIO) -import Test.Hspec (describe, it, Expectation, shouldBe, - shouldContain, Spec, expectationFailure, - shouldReturn) -import Test.QuickCheck (Arbitrary, arbitrary, shrink, property, elements) -import qualified Pipes.Prelude as P -import Data.Foldable (traverse_) +import Data.Aeson (encode) +import Data.Foldable (traverse_) +import Data.List (sort) +import GHC.Stack (HasCallStack) +import System.Console.ANSI + ( Color (..) + , ColorIntensity (Dull) + , ConsoleIntensity (BoldIntensity) + , ConsoleLayer (Foreground) + , SGR (SetColor, SetConsoleIntensity) + , setSGRCode + ) +import System.FilePath (()) +import System.IO.Unsafe (unsafePerformIO) +import Test.Hspec + ( Expectation + , Spec + , describe + , expectationFailure + , it + , shouldBe + , shouldContain + , shouldReturn + ) +import Test.QuickCheck (Arbitrary, arbitrary, elements, property, shrink) +import Text.Printf (printf) -import Argon +import Argon +import GHC.Data.FastString qualified as GHC +import GHC.Types.SrcLoc qualified as GHC instance Arbitrary ComplexityBlock where - arbitrary = (\a b c -> CC (a, b, c)) <$> arbitrary - <*> arbitrary - <*> arbitrary - shrink (CC t) = map CC $ shrink t + arbitrary = + (\a b c -> CC (a, b, c)) + <$> arbitrary + <*> arbitrary + <*> arbitrary + shrink (CC t) = map CC $ shrink t instance Arbitrary OutputMode where - arbitrary = elements [BareText, Colored, JSON] + arbitrary = elements [BareText, Colored, JSON] ones :: Loc ones = (1, 1) @@ -46,14 +54,15 @@ lo s = (s, 1) realSpan :: Int -> Int -> GHC.SrcSpan realSpan a b = GHC.mkSrcSpan (mkLoc a b) $ mkLoc (-a) (b + 24) - where mkLoc = GHC.mkSrcLoc (GHC.mkFastString "real loc") + where + mkLoc = GHC.mkSrcLoc (GHC.mkFastString "real loc") shouldContainErrors :: HasCallStack => FilePath -> [String] -> Expectation shouldContainErrors f errs = do - r <- analyze defaultConfig (path f) - case r of - (_, Right _) -> expectationFailure $ "Test did not fail" ++ show r - (_, Left msg) -> traverse_ (msg `shouldContain`) errs + r <- analyze defaultConfig (path f) + case r of + (_, Right _) -> expectationFailure $ "Test did not fail" ++ show r + (_, Left msg) -> traverse_ (msg `shouldContain`) errs path :: String -> FilePath path f = "test" "data" f @@ -63,25 +72,26 @@ shouldAnalyze f = shouldAnalyzeC (f, defaultConfig) shouldAnalyzeC :: HasCallStack => (String, Config) -> AnalysisResult -> Expectation shouldAnalyzeC (f, config) r = analyze config p `shouldReturn` (p, r) - where p = path f - --- Disabled until I figure out why Argon.Walker tests fail only on Travis -{-shouldProduceS :: Producer FilePath (SafeT IO) () -> [FilePath] -> Expectation-} -{-shouldProduceS prod res = do-} - {-paths <- runSafeT $ P.toListM prod-} - {-paths `shouldBe` res-} + where + p = path f -shouldProduce :: (Eq a, Show a) => Producer a IO () -> [a] -> Expectation -shouldProduce prod res = P.toListM prod >>= (`shouldBe` res) +-- shouldProduce :: (Eq a, Show a) => Producer a IO () -> [a] -> Expectation +-- shouldProduce prod res = P.toListM prod >>= (`shouldBe` res) -produceError, produceResult :: Producer (FilePath, AnalysisResult) IO () -produceError = each [("path/f.hs", Left "err!")] -produceResult = each [("f.hs", Right [ CC (ones, "g", 3) - , CC (lo 2, "h", 5) - , CC (lo 5, "f", 6) - , CC (lo 7, "m", 10) - , CC ((9, 2), "n", 15) - ])] +produceError, produceResult :: [(FilePath, AnalysisResult)] +produceError = [("path/f.hs", Left "err!")] +produceResult = + [ + ( "f.hs" + , Right + [ CC (ones, "g", 3) + , CC (lo 2, "h", 5) + , CC (lo 5, "f", 6) + , CC (lo 7, "m", 10) + , CC ((9, 2), "n", 15) + ] + ) + ] -- | ANSI bold color bold :: String @@ -97,228 +107,242 @@ reset = setSGRCode [] spec :: Spec spec = do - describe "analyze" $ do - it "accounts for case" $ - "case.hs" `shouldAnalyze` Right [CC (ones, "func", 3)] - it "accounts for if..then..else" $ - "ifthenelse.hs" `shouldAnalyze` Right [CC (ones, "f", 2)] - it "accounts for lambda case" $ - "lambdacase.hs" `shouldAnalyze` Right [CC (lo 2, "g", 3)] - it "accounts for multi way if" $ - "multiif.hs" `shouldAnalyze` Right [CC (lo 2, "f", 4)] - it "accounts for || operator" $ - "orop.hs" `shouldAnalyze` Right [CC (lo 1, "g", 3)] - it "accounts for && operator" $ - "andop.hs" `shouldAnalyze` Right [CC (lo 1, "g", 3)] - it "counts everything in a real example" $ - "stack-setup.hs" `shouldAnalyze` - Right [ CC (lo 3, "ensureCompiler", 14) - , CC ((4, 9), "wc", 1) - , CC ((21, 9), "needLocal", 4) - , CC ((27, 9), "isWanted", 1) - , CC ((41, 17), "installedCompiler", 2) - , CC ((81, 37), "tool", 1) - , CC ((94, 17), "idents", 1) - , CC ((103, 21), "m", 1) - ] - describe "extensions" $ do --- Not even GHC 7.8.4 is able to run the file below, so it's not an Argon bug -#if __GLASGOW_HASKELL__ >= 710 - it "correctly applies CPP" $ - "cpp-psyn.hs" `shouldAnalyze` Right [] -#endif - it "applies CPP when needed" $ - "cpp.hs" `shouldAnalyze` Right [CC (lo 5, "f", 4)] - it "works with TemplateHaskell" $ - "th.hs" `shouldAnalyze` Right [CC (lo 7, "foo", 1)] - it "works with DataKinds, GADTs, KindSignatures" $ - "datakinds.hs" `shouldAnalyze` - Right [ CC (lo 16, "taskOneWorker", 1) - , CC (lo 20, "main", 1)] - it "works with ScopedTypeVariables" $ - "scopedtypevariables.hs" `shouldAnalyze` - Right [CC (lo 9, "catchNonAsync", 1)] - it "works with TypeFamilies" $ - "typefamilies.hs" `shouldAnalyze` Right [] - it "works with ForeignImport" $ - "foreignimports.hs" `shouldAnalyze` Right [] - it "works with Arrows" $ - "arrows.hs" `shouldAnalyze` Right [CC (lo 7, "getAnchor", 1)] - describe "errors" $ do - it "catches syntax errors" $ - "syntaxerror.hs" `shouldContainErrors` - ["parse error (possibly incorrect indentation or mismatched brackets)"] - it "catches syntax errors (missing CPP)" $ - "missingcpp.hs" `shouldAnalyze` -#if __GLASGOW_HASKELL__ < 800 - Left "1:2 lexical error at character 'i'" -#else - Left "1:1 parse error on input \8216#\8217" -#endif -#if __GLASGOW_HASKELL__ < 800 --- The analysis of "missingmacros.hs" will succeed in newest GHC versions. - it "catches syntax errors (missing cabal macros)" $ - "missingmacros.hs" `shouldContainErrors` - ["error: missing binary operator before token "] -#endif - it "catches syntax errors (missing include dir)" $ - "missingincluded.hs" `shouldContainErrors` - ["fatal error", "necessaryInclude.h"] - it "catches CPP parsing errors" $ - "cpp-error.hs" `shouldContainErrors` - ["error: unterminated"] - describe "config" $ do - it "reads default extensions from Cabal file" $ - ("missingcpp.hs", unsafePerformIO - (do loadedExts <- parseExts $ path "test.cabal" - return $ defaultConfig { exts = loadedExts })) - `shouldAnalyzeC` - Right [CC (lo 4, "f", 1)] - it "reads other extensions from Cabal file" $ - ("missingcpp.hs", unsafePerformIO - (do loadedExts <- parseExts $ path "test-other.cabal" - return $ defaultConfig { exts = loadedExts })) - `shouldAnalyzeC` - Right [CC (lo 4, "f", 1)] - it "reads old extensions from Cabal file" $ - ("missingcpp.hs", unsafePerformIO - (do loadedExts <- parseExts $ path "test-old.cabal" - return $ defaultConfig { exts = loadedExts })) - `shouldAnalyzeC` - Right [CC (lo 4, "f", 1)] -#if __GLASGOW_HASKELL__ < 800 - it "includes Cabal macros for preprocessing" $ - ( "missingmacros.hs" - , defaultConfig { headers = [path "cabal_macros.h"] } - ) `shouldAnalyzeC` Right [CC (lo 3, "f", 2)] -#endif - it "includes directory from include-dir for preprocessing" $ - ( "missingincluded.hs" - , defaultConfig { includeDirs = [path "include"] } - ) `shouldAnalyzeC` Right [CC (lo 5, "f", 3)] - describe "Argon.Loc" $ do - describe "srcSpanToLoc" $ do - it "can convert a real src span to loc" $ - property $ \a b -> srcSpanToLoc (realSpan a b) == (a, b) - it "can convert a bad src span to loc" $ - srcSpanToLoc GHC.noSrcSpan `shouldBe` (0, 0) - describe "locToString" $ - it "can convert a loc to string" $ - locToString (1, 30) `shouldBe` "1:30" - describe "tagMsg" $ - it "can tag messages" $ - tagMsg (2, 3) "my custom msg" `shouldBe` "2:3 my custom msg" - describe "Argon.Results" $ do - describe "order" $ do - it "does not error on empty list" $ - order [] `shouldBe` [] - it "orders by complexity (descending)" $ - order [CC (ones, "f", 1), CC (lo 2, "f", 2)] `shouldBe` - [CC (lo 2, "f", 2), CC (ones, "f", 1)] - it "orders by lines (ascending)" $ - order [CC (lo 11, "f", 3), CC (ones, "f", 3)] `shouldBe` - [CC (ones, "f", 3), CC (lo 11, "f", 3)] - it "orders by function name (ascending)" $ - order [CC (lo 11, "g", 3), CC (lo 11, "f", 3)] `shouldBe` - [CC (lo 11, "f", 3), CC (lo 11, "g", 3)] - it "does not add or remove elements" $ - property $ \xs -> sort xs == sort (order xs) - it "is idempotent" $ - property $ \xs -> order xs == order (order xs) - describe "filterNulls" $ do - it "allows errors" $ - filterNulls ("", Left "err") `shouldBe` True - it "disallows empty results" $ - filterNulls ("", Right []) `shouldBe` False - it "always allows non-empty results" $ - property $ \x -> filterNulls ("", Right [x]) - describe "filterResults" $ do - it "discards results with too low complexity" $ - filterResults (Config 3 [] [] [] BareText ) - ("p", Right [ CC (ones, "f", 3) - , CC (lo 2, "g", 2) - , CC (lo 4, "h", 10) - , CC (lo 3, "l", 1)]) - `shouldBe` - ("p", Right [ CC (lo 4, "h", 10) - , CC (ones, "f", 3)]) - it "does nothing on Left" $ - property $ \m o p err -> filterResults (Config m [] [] [] o) - (p, Left err) == - (p, Left err) - describe "Argon.Formatters" $ do - describe "bareTextFormatter" $ do - it "correctly formats errors" $ - (produceError >-> bareTextFormatter) `shouldProduce` - ["path/f.hs", "\terror: err!"] - it "correctly formats results" $ - (produceResult >-> bareTextFormatter) `shouldProduce` - [ "f.hs" - , "\t1:1 g - 3" - , "\t2:1 h - 5" - , "\t5:1 f - 6" - , "\t7:1 m - 10" - , "\t9:2 n - 15" - ] - describe "coloredTextFormatter" $ do - it "correctly formats errors" $ - (produceError >-> coloredTextFormatter) `shouldProduce` - [ bold ++ "path/f.hs" ++ reset - , "\t" ++ fore Red ++ "error" ++ reset ++ ": err!" - ] - it "correctly formats results" $ - (produceResult >-> coloredTextFormatter) `shouldProduce` - [ bold ++ "f.hs" ++ reset - , printf "\t1:1 %sg%s - %sA (3)%s" (fore Cyan) reset - (fore Green) reset - , printf "\t2:1 %sh%s - %sA (5)%s" (fore Cyan) reset - (fore Green) reset - , printf "\t5:1 %sf%s - %sB (6)%s" (fore Cyan) reset - (fore Yellow) reset - , printf "\t7:1 %sm%s - %sB (10)%s" (fore Cyan) reset - (fore Yellow) reset - , printf "\t9:2 %sn%s - %sC (15)%s" (fore Magenta) reset - (fore Red) reset - ] - describe "Argon.Types" $ do - describe "ComplexityBlock" $ do - it "implements Show correctly" $ - show (CC ((2, 3), "bla bla", 32)) `shouldBe` - "CC ((2,3),\"bla bla\",32)" - it "implements Eq correctly" $ - CC ((1, 4), "fun", 13) `shouldBe` CC ((1, 4), "fun", 13) - it "implements Ord correctly" $ - CC (lo 1, "g", 2) < CC (lo 2, "f", 1) `shouldBe` True - describe "OutputMode" $ do - it "implements Show correctly" $ - show [JSON, Colored, BareText] `shouldBe` - "[JSON,Colored,BareText]" - it "implements Eq correctly" $ - [JSON, Colored, BareText] `shouldBe` [JSON, Colored, BareText] - describe "ToJSON instance" $ do - it "is implemented by ComplexityResult" $ - encode (CC ((1, 3), "f", 4)) `shouldBe` - "{\"complexity\":4,\"name\":\"f\",\"lineno\":1,\"col\":3}" - it "is implemented by (FilePath, AnalysisResult)" $ - encode ("f.hs" :: String, Right [] :: AnalysisResult) - `shouldBe` - "{\"blocks\":[],\"path\":\"f.hs\",\"type\":\"result\"}" - it "is implemented by (FilePath, AnalysisResult) II" $ - encode ("f.hs" :: String, Left "err" :: AnalysisResult) - `shouldBe` - "{\"path\":\"f.hs\",\"type\":\"error\",\"message\":\"err\"}" -#if 0 - describe "Argon.Walker" $ - describe "allFiles" $ do - it "traverses the filesystem depth-first" $ - allFiles ("test" "tree") `shouldProduceS` - [ "test" "tree" "sub" "b.hs" - , "test" "tree" "sub" "c.hs" - , "test" "tree" "sub2" "a.hs" - , "test" "tree" "sub2" "e.hs" - , "test" "tree" "a.hs" - ] - it "includes starting files in the result" $ - allFiles ("test" "tree" "a.hs") `shouldProduceS` - ["test" "tree" "a.hs"] -#endif + describe "analyze" $ do + it "accounts for case" $ + "case.hs" `shouldAnalyze` Right [CC (ones, "func", 3)] + it "accounts for if..then..else" $ + "ifthenelse.hs" `shouldAnalyze` Right [CC (ones, "f", 2)] + it "accounts for lambda case" $ + "lambdacase.hs" `shouldAnalyze` Right [CC (lo 2, "g", 3)] + it "accounts for multi way if" $ + "multiif.hs" `shouldAnalyze` Right [CC (lo 2, "f", 4)] + it "accounts for || operator" $ + "orop.hs" `shouldAnalyze` Right [CC (lo 1, "g", 3)] + it "accounts for && operator" $ + "andop.hs" `shouldAnalyze` Right [CC (lo 1, "g", 3)] + it "counts everything in a real example" $ + "stack-setup.hs" + `shouldAnalyze` Right + [ CC (lo 3, "ensureCompiler", 14) + , CC ((4, 9), "wc", 1) + , CC ((21, 9), "needLocal", 4) + , CC ((27, 9), "isWanted", 1) + , CC ((41, 17), "installedCompiler", 2) + , CC ((81, 37), "tool", 1) + , CC ((94, 17), "idents", 1) + , CC ((103, 21), "m", 1) + ] + describe "extensions" $ do + -- Not even GHC 7.8.4 is able to run the file below, so it's not an Argon bug + it "correctly applies CPP" $ + "cpp-psyn.hs" `shouldAnalyze` Right [] + it "applies CPP when needed" $ + "cpp.hs" `shouldAnalyze` Right [CC (lo 5, "f", 4)] + it "works with TemplateHaskell" $ + "th.hs" `shouldAnalyze` Right [CC (lo 7, "foo", 1)] + it "works with DataKinds, GADTs, KindSignatures" $ + "datakinds.hs" + `shouldAnalyze` Right + [ CC (lo 16, "taskOneWorker", 1) + , CC (lo 20, "main", 1) + ] + it "works with ScopedTypeVariables" $ + "scopedtypevariables.hs" + `shouldAnalyze` Right [CC (lo 9, "catchNonAsync", 1)] + it "works with TypeFamilies" $ + "typefamilies.hs" `shouldAnalyze` Right [] + it "works with ForeignImport" $ + "foreignimports.hs" `shouldAnalyze` Right [] + it "works with Arrows" $ + "arrows.hs" `shouldAnalyze` Right [CC (lo 7, "getAnchor", 1)] + describe "errors" $ do + it "catches syntax errors" $ + "syntaxerror.hs" + `shouldContainErrors` ["parse error (possibly incorrect indentation or mismatched brackets)"] + it "catches syntax errors (missing CPP)" $ + "missingcpp.hs" + `shouldContainErrors` ["parse error on input `#'"] + -- These two are not errors anymore, cpphs gracefully continues + -- doing its thing after warning about the bad CPP code + -- it "catches syntax errors (missing include dir)" $ + -- "missingincluded.hs" `shouldContainErrors` + -- ["fatal error", "necessaryInclude.h"] + -- it "catches CPP parsing errors" $ + -- "cpp-error.hs" `shouldContainErrors` + -- ["error: unterminated"] + describe "config" $ do + it "reads default extensions from Cabal file" $ + ( "missingcpp.hs" + , unsafePerformIO + ( do + loadedExts <- parseExts $ path "test.cabal" + return $ defaultConfig {exts = loadedExts} + ) + ) + `shouldAnalyzeC` Right [CC (lo 4, "f", 1)] + it "reads other extensions from Cabal file" $ + ( "missingcpp.hs" + , unsafePerformIO + ( do + loadedExts <- parseExts $ path "test-other.cabal" + return $ defaultConfig {exts = loadedExts} + ) + ) + `shouldAnalyzeC` Right [CC (lo 4, "f", 1)] + it "reads old extensions from Cabal file" $ + ( "missingcpp.hs" + , unsafePerformIO + ( do + loadedExts <- parseExts $ path "test-old.cabal" + return $ defaultConfig {exts = loadedExts} + ) + ) + `shouldAnalyzeC` Right [CC (lo 4, "f", 1)] + it "includes directory from include-dir for preprocessing" $ + ( "missingincluded.hs" + , defaultConfig {includeDirs = [path "include"]} + ) + `shouldAnalyzeC` Right [CC (lo 5, "f", 3)] + describe "Argon.Loc" $ do + describe "srcSpanToLoc" $ do + it "can convert a real src span to loc" $ + property $ + \a b -> srcSpanToLoc (realSpan a b) == (a, b) + it "can convert a bad src span to loc" $ + srcSpanToLoc GHC.noSrcSpan `shouldBe` (0, 0) + describe "locToString" $ + it "can convert a loc to string" $ + locToString (1, 30) `shouldBe` "1:30" + describe "tagMsg" $ + it "can tag messages" $ + tagMsg (2, 3) "my custom msg" `shouldBe` "2:3 my custom msg" + describe "Argon.Results" $ do + describe "order" $ do + it "does not error on empty list" $ + order [] `shouldBe` [] + it "orders by complexity (descending)" $ + order [CC (ones, "f", 1), CC (lo 2, "f", 2)] + `shouldBe` [CC (lo 2, "f", 2), CC (ones, "f", 1)] + it "orders by lines (ascending)" $ + order [CC (lo 11, "f", 3), CC (ones, "f", 3)] + `shouldBe` [CC (ones, "f", 3), CC (lo 11, "f", 3)] + it "orders by function name (ascending)" $ + order [CC (lo 11, "g", 3), CC (lo 11, "f", 3)] + `shouldBe` [CC (lo 11, "f", 3), CC (lo 11, "g", 3)] + it "does not add or remove elements" $ + property $ + \xs -> sort xs == sort (order xs) + it "is idempotent" $ + property $ + \xs -> order xs == order (order xs) + describe "filterNulls" $ do + it "allows errors" $ + filterNulls ("", Left "err") `shouldBe` True + it "disallows empty results" $ + filterNulls ("", Right []) `shouldBe` False + it "always allows non-empty results" $ + property $ + \x -> filterNulls ("", Right [x]) + describe "filterResults" $ do + it "discards results with too low complexity" $ + filterResults + (Config 3 [] [] [] BareText) + ( "p" + , Right + [ CC (ones, "f", 3) + , CC (lo 2, "g", 2) + , CC (lo 4, "h", 10) + , CC (lo 3, "l", 1) + ] + ) + `shouldBe` ( "p" + , Right + [ CC (lo 4, "h", 10) + , CC (ones, "f", 3) + ] + ) + it "does nothing on Left" $ + property $ \m o p err -> + filterResults + (Config m [] [] [] o) + (p, Left err) + == (p, Left err) + describe "Argon.Formatters" $ do + describe "bareTextFormatter" $ do + it "correctly formats errors" $ + (bareTextFormatter produceError) + `shouldBe` ["path/f.hs", "\terror: err!"] + it "correctly formats results" $ + (bareTextFormatter produceResult) + `shouldBe` [ "f.hs" + , "\t1:1 g - 3" + , "\t2:1 h - 5" + , "\t5:1 f - 6" + , "\t7:1 m - 10" + , "\t9:2 n - 15" + ] + describe "coloredTextFormatter" $ do + it "correctly formats errors" $ + (coloredTextFormatter produceError) + `shouldBe` [ bold ++ "path/f.hs" ++ reset + , "\t" ++ fore Red ++ "error" ++ reset ++ ": err!" + ] + it "correctly formats results" $ + (coloredTextFormatter produceResult) + `shouldBe` [ bold ++ "f.hs" ++ reset + , printf + "\t1:1 %sg%s - %sA (3)%s" + (fore Cyan) + reset + (fore Green) + reset + , printf + "\t2:1 %sh%s - %sA (5)%s" + (fore Cyan) + reset + (fore Green) + reset + , printf + "\t5:1 %sf%s - %sB (6)%s" + (fore Cyan) + reset + (fore Yellow) + reset + , printf + "\t7:1 %sm%s - %sB (10)%s" + (fore Cyan) + reset + (fore Yellow) + reset + , printf + "\t9:2 %sn%s - %sC (15)%s" + (fore Magenta) + reset + (fore Red) + reset + ] + describe "Argon.Types" $ do + describe "ComplexityBlock" $ do + it "implements Show correctly" $ + show (CC ((2, 3), "bla bla", 32)) + `shouldBe` "CC ((2,3),\"bla bla\",32)" + it "implements Eq correctly" $ + CC ((1, 4), "fun", 13) `shouldBe` CC ((1, 4), "fun", 13) + it "implements Ord correctly" $ + CC (lo 1, "g", 2) < CC (lo 2, "f", 1) `shouldBe` True + describe "OutputMode" $ do + it "implements Show correctly" $ + show [JSON, Colored, BareText] + `shouldBe` "[JSON,Colored,BareText]" + it "implements Eq correctly" $ + [JSON, Colored, BareText] `shouldBe` [JSON, Colored, BareText] + describe "ToJSON instance" $ do + it "is implemented by ComplexityResult" $ + encode (CC ((1, 3), "f", 4)) + `shouldBe` "{\"col\":3,\"complexity\":4,\"lineno\":1,\"name\":\"f\"}" + it "is implemented by (FilePath, AnalysisResult)" $ + encode ("f.hs" :: String, Right [] :: AnalysisResult) + `shouldBe` "{\"blocks\":[],\"path\":\"f.hs\",\"type\":\"result\"}" + it "is implemented by (FilePath, AnalysisResult) II" $ + encode ("f.hs" :: String, Left "err" :: AnalysisResult) + `shouldBe` "{\"message\":\"err\",\"path\":\"f.hs\",\"type\":\"error\"}" diff --git a/test/HLint.hs b/test/HLint.hs deleted file mode 100644 index cd2767b..0000000 --- a/test/HLint.hs +++ /dev/null @@ -1,15 +0,0 @@ -module Main (main) where - -import Language.Haskell.HLint (hlint) -import System.Exit (exitFailure, exitSuccess) - -arguments :: [String] -arguments = - [ "app" - , "src" - ] - -main :: IO () -main = do - hints <- hlint arguments - if null hints then exitSuccess else exitFailure