From 2d0635ebcffd55aaf59572efe3c58c47ca41fb60 Mon Sep 17 00:00:00 2001 From: "Greg Mat." <65409906+itsvyle@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:48:33 +0000 Subject: [PATCH 1/5] Added support for staticcheck linter --- .github/workflows/test.yml | 8 ++- README.md | 2 + action.yml | 20 ++++++ src/linters/staticcheck.js | 77 ++++++++++++++++++++++ test/linters/linters.test.js | 2 + test/linters/params/staticcheck.js | 72 ++++++++++++++++++++ test/linters/projects/staticcheck/file1.go | 37 +++++++++++ test/linters/projects/staticcheck/file2.go | 34 ++++++++++ test/linters/projects/staticcheck/go.mod | 3 + 9 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/linters/staticcheck.js create mode 100644 test/linters/params/staticcheck.js create mode 100644 test/linters/projects/staticcheck/file1.go create mode 100644 test/linters/projects/staticcheck/file2.go create mode 100644 test/linters/projects/staticcheck/go.mod diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0849f0b9..cb6d27aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,14 +44,16 @@ jobs: # Go - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.19.4 + go-version: 1.21.6 - name: Install Go dependencies run: | - cd ./test/linters/projects/golint + cd ./test/linters/projects/golint/ go install golang.org/x/lint/golint@latest + cd ../staticcheck/ + go install honnef.co/go/tools/cmd/staticcheck@latest # Node.js diff --git a/README.md b/README.md index e4a08da4..2eece3c8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ _**Note:** The behavior of actions like this one is currently limited in the con - **Go:** - [gofmt](https://golang.org/cmd/gofmt) - [golint](https://github.com/golang/lint) + - [staticcheck](https://staticcheck.dev/) - **JavaScript:** - [ESLint](https://eslint.org) - [Prettier](https://prettier.io) @@ -437,6 +438,7 @@ Some options are not available for specific linters: | flake8 | ❌ | ✅ | | gofmt | ✅ | ❌ (go) | | golint | ❌ | ❌ (go) | +| staticcheck | ❌ | ❌ (go) | | mypy | ❌ | ❌ (py) | | oitnb | ✅ | ✅ | | php_codesniffer | ❌ | ✅ | diff --git a/action.yml b/action.yml index 0bc82a37..c63001c7 100644 --- a/action.yml +++ b/action.yml @@ -120,6 +120,26 @@ inputs: required: false default: "false" + staticcheck: + description: Enable or disable staticcheck checks + required: false + default: "false" + staticcheck_dir: + description: Directory where the staticcheck command should be run + required: false + staticcheck_args: + description: Additional arguments to pass to the linter + required: false + default: "" + staticcheck_extensions: + description: Extensions of files to check with staticcheck + required: false + default: "go" + staticcheck_command_prefix: + description: Shell command to prepend to the linter command + required: false + default: "" + # JavaScript eslint: diff --git a/src/linters/staticcheck.js b/src/linters/staticcheck.js new file mode 100644 index 00000000..4e2290a3 --- /dev/null +++ b/src/linters/staticcheck.js @@ -0,0 +1,77 @@ +const { run } = require("../utils/action"); +const commandExists = require("../utils/command-exists"); +const { initLintResult } = require("../utils/lint-result"); +const { capitalizeFirstLetter } = require("../utils/string"); + +const PARSE_REGEX = /^(.+):([0-9]+):[0-9]+: (.+)$/gm; + +/** @typedef {import('../utils/lint-result').LintResult} LintResult */ + +/** + * https://github.com/golang/lint + */ +class Staticcheck { + static get name() { + return "staticcheck"; + } + + /** + * Verifies that all required programs are installed. Throws an error if programs are missing + * @param {string} dir - Directory to run the linting program in + * @param {string} prefix - Prefix to the lint command + */ + static async verifySetup(dir, prefix = "") { + // Verify that golint is installed + if (!(await commandExists("staticcheck"))) { + throw new Error(`${this.name} is not installed`); + } + } + + /** + * Runs the linting program and returns the command output + * @param {string} dir - Directory to run the linter in + * @param {string[]} extensions - File extensions which should be linted + * @param {string} args - Additional arguments to pass to the linter + * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically + * @param {string} prefix - Prefix to the lint command + * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command + */ + static lint(dir, extensions, args = "", fix = false, prefix = "") { + if (extensions.length !== 1 || extensions[0] !== "go") { + throw new Error(`${this.name} error: File extensions are not configurable`); + } + + return run(`${prefix} staticcheck -f text ${args} "./..."`, { + dir, + ignoreErrors: true, + }); + } + + /** + * Parses the output of the lint command. Determines the success of the lint process and the + * severity of the identified code style violations + * @param {string} dir - Directory in which the linter has been run + * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command + * @returns {LintResult} - Parsed lint result + */ + static parseOutput(dir, output) { + const lintResult = initLintResult(); + lintResult.isSuccess = output.status === 0; + + const matches = output.stdout.matchAll(PARSE_REGEX); + for (const match of matches) { + const [_, path, line, text] = match; + const lineNr = parseInt(line, 10); + lintResult.error.push({ + path, + firstLine: lineNr, + lastLine: lineNr, + message: capitalizeFirstLetter(text), + }); + } + + return lintResult; + } +} + +module.exports = Staticcheck; diff --git a/test/linters/linters.test.js b/test/linters/linters.test.js index 6c7366b9..3897e068 100644 --- a/test/linters/linters.test.js +++ b/test/linters/linters.test.js @@ -20,6 +20,7 @@ const prettierParams = require("./params/prettier"); const pylintParams = require("./params/pylint"); const ruboCopParams = require("./params/rubocop"); const rustfmtParams = require("./params/rustfmt"); +const staticcheckParams = require("./params/staticcheck"); const stylelintParams = require("./params/stylelint"); const swiftFormatLockwood = require("./params/swift-format-lockwood"); // const swiftFormatOfficial = require("./params/swift-format-official"); @@ -39,6 +40,7 @@ const linterParams = [ flake8Params, gofmtParams, golintParams, + staticcheckParams, mypyParams, phpCodeSnifferParams, prettierParams, diff --git a/test/linters/params/staticcheck.js b/test/linters/params/staticcheck.js new file mode 100644 index 00000000..3c1a29aa --- /dev/null +++ b/test/linters/params/staticcheck.js @@ -0,0 +1,72 @@ +const Staticcheck = require("../../../src/linters/staticcheck"); + +const testName = "staticcheck"; +const linter = Staticcheck; +const commandPrefix = ""; +const args = ""; +const extensions = ["go"]; + +// Linting without auto-fixing +function getLintParams(dir) { + const stdoutFile1 = + "file1.go:20:2: this value of err is never used (SA4006)\nfile1.go:20:9: New doesn't have side effects and its return value is ignored (SA4017)\nfile1.go:31:6: func main1 is unused (U1000)"; + const stdoutFile2 = `file2.go:11:3: this linter directive didn't match anything; should it be removed? (staticcheck)\nfile2.go:12:19: calling regexp.MatchString in a loop has poor performance, consider using regexp.Compile (SA6000)\nfile2.go:25:6: func main2 is unused (U1000)`; + return { + // Expected output of the linting function + cmdOutput: { + status: 1, + stdoutParts: [stdoutFile1, stdoutFile2], + stdout: `${stdoutFile1}\n${stdoutFile2}`, + }, + // Expected output of the parsing function + lintResult: { + isSuccess: false, + warning: [], + error: [ + { + path: "file1.go", + firstLine: 20, + lastLine: 20, + message: "This value of err is never used (SA4006)", + }, + { + path: "file1.go", + firstLine: 20, + lastLine: 20, + message: "New doesn't have side effects and its return value is ignored (SA4017)", + }, + { + path: "file1.go", + firstLine: 31, + lastLine: 31, + message: "Func main1 is unused (U1000)", + }, + { + path: "file2.go", + firstLine: 11, + lastLine: 11, + message: + "This linter directive didn't match anything; should it be removed? (staticcheck)", + }, + { + path: "file2.go", + firstLine: 12, + lastLine: 12, + message: + "Calling regexp.MatchString in a loop has poor performance, consider using regexp.Compile (SA6000)", + }, + { + path: "file2.go", + firstLine: 25, + lastLine: 25, + message: "Func main2 is unused (U1000)", + }, + ], + }, + }; +} + +// Linting with auto-fixing +const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect + +module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; diff --git a/test/linters/projects/staticcheck/file1.go b/test/linters/projects/staticcheck/file1.go new file mode 100644 index 00000000..a6fa6132 --- /dev/null +++ b/test/linters/projects/staticcheck/file1.go @@ -0,0 +1,37 @@ +package main + +import ( + "errors" + "fmt" + "log" +) + +type Result struct { + Entries []string +} + +func Query() (Result, error) { + return Result{ + Entries: []string{}, + }, nil +} + +func ResultEntries() (Result, error) { + err := errors.New("no entries found") + result, err := Query() + if err != nil { + return Result{}, err + } + if len(result.Entries) == 0 { + return Result{}, err + } + return result, nil +} + +func main1() { + result, err := ResultEntries() + if err != nil { + log.Fatal(err) + } + fmt.Printf("result=%v, err=%v", result, err) +} diff --git a/test/linters/projects/staticcheck/file2.go b/test/linters/projects/staticcheck/file2.go new file mode 100644 index 00000000..66a5bbf9 --- /dev/null +++ b/test/linters/projects/staticcheck/file2.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "log" + "regexp" +) + +func ValidateEmails(addrs []string) (bool, error) { + for _, email := range addrs { + //lint:ignore SA1000 we love invalid regular expressions! + matched, err := regexp.MatchString("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]*$", email) + if err != nil { + return false, err + } + + if !matched { + return false, nil + } + } + + return true, nil +} + +func main2() { + emails := []string{"testuser@gmail.com", "anotheruser@yahoo.com", "onemoreuser@hotmail.com"} + + matched, err := ValidateEmails(emails) + if err != nil { + log.Fatal(err) + } + + fmt.Println(matched) +} \ No newline at end of file diff --git a/test/linters/projects/staticcheck/go.mod b/test/linters/projects/staticcheck/go.mod new file mode 100644 index 00000000..57178014 --- /dev/null +++ b/test/linters/projects/staticcheck/go.mod @@ -0,0 +1,3 @@ +module linting-test + +go 1.21.6 From 5e4a21c62125050be9ccf74d2b22c63fbf11b4c5 Mon Sep 17 00:00:00 2001 From: Greg Mat <65409906+itsvyle@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:24:35 +0100 Subject: [PATCH 2/5] Added the linter to the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2eece3c8..92757e3d 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ With `auto_fix` set to `true`, by default the action will try and fix code issue ### Linter-specific options -`[linter]` can be one of `autopep8`, `black`, `clang_format`, `dotnet_format`, `erblint`, `eslint`, `flake8`, `gofmt`, `golint`, `mypy`, `oitnb`, `php_codesniffer`, `prettier`, `pylint`, `rubocop`, `stylelint`, `swift_format_official`, `swift_format_lockwood`, `swiftlint` and `xo`: +`[linter]` can be one of `autopep8`, `black`, `clang_format`, `dotnet_format`, `erblint`, `eslint`, `flake8`, `gofmt`, `golint`, `mypy`, `oitnb`, `php_codesniffer`, `prettier`, `pylint`, `staticcheck`, `rubocop`, `stylelint`, `swift_format_official`, `swift_format_lockwood`, `swiftlint` and `xo`: - **`[linter]`:** Enables the linter in your repository. Default: `false` - **`[linter]_args`**: Additional arguments to pass to the linter. Example: `eslint_args: "--max-warnings 0"` if ESLint checks should fail even if there are no errors and only warnings. Default: `""` From c8e7f0f07cafe20b28f821cc711a0fefa9574187 Mon Sep 17 00:00:00 2001 From: Greg Mat <65409906+itsvyle@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:09:36 +0100 Subject: [PATCH 3/5] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2862ca33..fa520538 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,8 @@ jobs: - name: Check out Git repository uses: actions/checkout@v3 with: - # Custom token to allow commits trigger other workflows. - token: ${{ secrets.BUILD_ACTION_GITHUB_TOKEN }} + # # Custom token to allow commits trigger other workflows. + # token: ${{ secrets.BUILD_ACTION_GITHUB_TOKEN }} - name: Set up Node.js uses: actions/setup-node@v3 From 42ab895321ae246ae294b0892e23a06d3b54db7f Mon Sep 17 00:00:00 2001 From: Greg Mat <65409906+itsvyle@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:11:03 +0100 Subject: [PATCH 4/5] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa520538..2862ca33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,8 @@ jobs: - name: Check out Git repository uses: actions/checkout@v3 with: - # # Custom token to allow commits trigger other workflows. - # token: ${{ secrets.BUILD_ACTION_GITHUB_TOKEN }} + # Custom token to allow commits trigger other workflows. + token: ${{ secrets.BUILD_ACTION_GITHUB_TOKEN }} - name: Set up Node.js uses: actions/setup-node@v3 From c41e029f653b39d1f7ad04383d0acd0f87059011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mat?= <65409906+itsvyle@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:32:43 +0000 Subject: [PATCH 5/5] Added staticcheck to actual linter --- src/linters/index.js | 58 +++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/linters/index.js b/src/linters/index.js index 800ce798..a6025e8d 100644 --- a/src/linters/index.js +++ b/src/linters/index.js @@ -19,40 +19,42 @@ const Stylelint = require("./stylelint"); const SwiftFormatLockwood = require("./swift-format-lockwood"); const SwiftFormatOfficial = require("./swift-format-official"); const SwiftLint = require("./swiftlint"); +const StaticCheck = require("./staticcheck"); const TSC = require("./tsc"); const XO = require("./xo"); const linters = { - // Linters - clippy: Clippy, - erblint: Erblint, - eslint: ESLint, - flake8: Flake8, - golint: Golint, - mypy: Mypy, - php_codesniffer: PHPCodeSniffer, - pylint: Pylint, - rubocop: RuboCop, - stylelint: Stylelint, - swiftlint: SwiftLint, - xo: XO, - tsc: TSC, + // Linters + clippy: Clippy, + erblint: Erblint, + eslint: ESLint, + flake8: Flake8, + golint: Golint, + mypy: Mypy, + php_codesniffer: PHPCodeSniffer, + pylint: Pylint, + rubocop: RuboCop, + stylelint: Stylelint, + swiftlint: SwiftLint, + staticcheck: StaticCheck, + xo: XO, + tsc: TSC, - // Formatters (should be run after linters) - autopep8: Autopep8, - black: Black, - clang_format: ClangFormat, - dotnet_format: DotnetFormat, - gofmt: Gofmt, - oitnb: Oitnb, - rustfmt: RustFmt, - prettier: Prettier, - swift_format_lockwood: SwiftFormatLockwood, - swift_format_official: SwiftFormatOfficial, + // Formatters (should be run after linters) + autopep8: Autopep8, + black: Black, + clang_format: ClangFormat, + dotnet_format: DotnetFormat, + gofmt: Gofmt, + oitnb: Oitnb, + rustfmt: RustFmt, + prettier: Prettier, + swift_format_lockwood: SwiftFormatLockwood, + swift_format_official: SwiftFormatOfficial, - // Alias of `swift_format_lockwood` (for backward compatibility) - // TODO: Remove alias in v2 - swiftformat: SwiftFormatLockwood, + // Alias of `swift_format_lockwood` (for backward compatibility) + // TODO: Remove alias in v2 + swiftformat: SwiftFormatLockwood, }; module.exports = linters;