diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ef66323..c8277803 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: # Go - - name: Install Go + - name: Set up Go uses: actions/setup-go@v1 with: go-version: 1.13 @@ -38,12 +38,12 @@ jobs: # Node.js - - name: Install Node.js and Yarn + - name: Set up Node.js uses: actions/setup-node@v1 with: node-version: 12 - - name: Install NPM dependencies + - name: Install Node.js dependencies run: | cd ./test/linters/projects/eslint yarn install @@ -56,22 +56,21 @@ jobs: # Python - - name: Install Python and pip + - name: Set up Python uses: actions/setup-python@v1 with: python-version: 3.8 - - name: Install Black - run: pip install black - - - name: Install Flake8 - run: pip install flake8 + - name: Install Python dependencies + run: pip install black flake8 # Swift (only on macOS) - - name: Install SwiftLint + - name: Install Swift dependencies if: startsWith(matrix.os, 'macos') - run: brew install swiftlint + run: | + brew update + brew install swiftformat swiftlint # Tests diff --git a/README.md b/README.md index 35fc04b0..4306473b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This action… - [Black](https://black.readthedocs.io) - [Flake8](http://flake8.pycqa.org) - **Swift:** + - [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) - [SwiftLint](https://github.com/realm/SwiftLint) ## Usage @@ -137,7 +138,7 @@ All linters are disabled by default. To enable a linter, simply set the option w eslint: true # Enables ESLint checks ``` -`[linter]` can be one of `black`, `eslint`, `flake8`, `gofmt`, `golint`, `prettier`, `stylelint`, and `swiftlint`: +`[linter]` can be one of `black`, `eslint`, `flake8`, `gofmt`, `golint`, `prettier`, `stylelint`, `swiftformat` and `swiftlint`: - **`[linter]`:** Enables the linter in your repository. Default: `false` - **`[linter]_extensions`:** Extensions of files to check with the linter. Example: `eslint_extensions: js,ts` to lint both JavaScript and TypeScript files with ESLint. Default: See [`action.yml`](./action.yml) diff --git a/action.yml b/action.yml index 33c325e8..c505eac4 100644 --- a/action.yml +++ b/action.yml @@ -102,6 +102,18 @@ inputs: # Swift + swiftformat: + description: Enable or disable SwiftFormat checks + required: false + default: false + swiftformat_extensions: + description: Extensions of files to check with SwiftFormat + required: false + default: "swift" + swiftformat_dir: + description: Directory where the SwiftFormat command should be run + required: false + swiftlint: description: Enable or disable SwiftLint checks required: false diff --git a/src/linters/swiftformat.js b/src/linters/swiftformat.js new file mode 100644 index 00000000..011261f1 --- /dev/null +++ b/src/linters/swiftformat.js @@ -0,0 +1,77 @@ +const commandExists = require("../../vendor/command-exists"); +const { run } = require("../utils/action"); + +const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: \w+: \((\w+)\) (.*)\.$/gm; + +/** + * https://github.com/nicklockwood/SwiftFormat + */ +class SwiftFormat { + static get name() { + return "SwiftFormat"; + } + + /** + * Verifies that all required programs are installed. Exits the GitHub action if one of the + * programs is missing + * + * @param {string} dir: Directory to run the linting program in + */ + static async verifySetup(dir) { + // Verify that SwiftFormat is installed + if (!(await commandExists("swiftformat"))) { + throw new Error(`${this.name} is not installed`); + } + } + + /** + * Runs the linting program and returns the command output + * + * @param {string} dir: Directory to run the linting program in + * @param {string[]} extensions: Array of file extensions which should be linted + * @param {boolean} fix: Whether the linter should attempt to fix code style issues automatically + * @returns {string}: Results of the linting process + */ + static lint(dir, extensions, fix = false) { + if (extensions.length !== 1 || extensions[0] !== "swift") { + throw new Error(`${this.name} error: File extensions are not configurable`); + } + + return run(`swiftformat ${fix ? "" : "--lint"} "."`, { + dir, + ignoreErrors: true, + }).stderr; + } + + /** + * Parses the results of the linting process and returns it as a processable array + * + * @param {string} dir: Directory in which the linting program has been run + * @param {string} results: Results of the linting process + * @returns {object[]}: Parsed results + */ + static parseResults(dir, results) { + const matches = results.matchAll(PARSE_REGEX); + + // Parsed results: [notices, warnings, failures] + const resultsParsed = [[], [], []]; + + for (const match of matches) { + const [_, pathFull, line, rule, message] = match; + const path = pathFull.substring(dir.length + 1); + const lineNr = parseInt(line, 10); + // SwiftFormat only seems to use the "warning" level, which this action will therefore + // categorize as errors + resultsParsed[2].push({ + path, + firstLine: lineNr, + lastLine: lineNr, + message: `${message} (${rule})`, + }); + } + + return resultsParsed; + } +} + +module.exports = SwiftFormat; diff --git a/test/linters/linters.test.js b/test/linters/linters.test.js index 52eddae5..ef015943 100644 --- a/test/linters/linters.test.js +++ b/test/linters/linters.test.js @@ -9,6 +9,7 @@ const gofmtParams = require("./params/gofmt"); const golintParams = require("./params/golint"); const prettierParams = require("./params/prettier"); const stylelintParams = require("./params/stylelint"); +const swiftformatParams = require("./params/swiftformat"); const swiftlintParams = require("./params/swiftlint"); const linterParams = [ @@ -24,7 +25,7 @@ const linterParams = [ // Only run Swift tests on macOS if (process.platform === "darwin") { - linterParams.push(swiftlintParams); + linterParams.push(swiftformatParams, swiftlintParams); } describe.each(linterParams)( diff --git a/test/linters/params/swiftformat.js b/test/linters/params/swiftformat.js new file mode 100644 index 00000000..9ade0575 --- /dev/null +++ b/test/linters/params/swiftformat.js @@ -0,0 +1,65 @@ +const { join } = require("path"); +const SwiftFormat = require("../../../src/linters/swiftformat"); + +const testName = "swiftformat"; +const linter = SwiftFormat; +const extensions = ["swift"]; + +// Testing input/output for the Linter.lint function, with auto-fixing disabled +function getLintParams(dir) { + const warning1 = `${join( + dir, + "file1.swift", + )}:3:1: warning: (consecutiveBlankLines) Replace consecutive blank lines with a single blank line.`; + const warning2 = `${join( + dir, + "file1.swift", + )}:7:1: warning: (indent) Indent code in accordance with the scope level.`; + const warning3 = `${join(dir, "file2.swift")}:2:1: warning: (semicolons) Remove semicolons.`; + return { + // Strings that must be contained in the stdout of the lint command + stdoutParts: [warning1, warning2, warning3], + // Example output of the lint command, used to test the parsing function + parseInput: `Running SwiftFormat...\n(lint mode - no files will be changed.)\n${warning1}\n${warning2}\n${warning3}\nwarning: No swift version was specified, so some formatting features were disabled. Specify the version of swift you are using with the --swiftversion command line option, or by adding a .swift-version file to your project.\nSwiftFormat completed in 0.01s.\nSource input did not pass lint check.\n2/2 files require formatting.`, + // Expected output of the parsing function + parseResult: [ + [], + [], + [ + { + path: "file1.swift", + firstLine: 3, + lastLine: 3, + message: + "Replace consecutive blank lines with a single blank line (consecutiveBlankLines)", + }, + { + path: "file1.swift", + firstLine: 7, + lastLine: 7, + message: "Indent code in accordance with the scope level (indent)", + }, + { + path: "file2.swift", + firstLine: 2, + lastLine: 2, + message: "Remove semicolons (semicolons)", + }, + ], + ], + }; +} + +// Testing input/output for the Linter.lint function, with auto-fixing enabled +function getFixParams(dir) { + return { + // stdout of the lint command + stdout: "", + // Example output of the lint command, used to test the parsing function + parseInput: "", + // Expected output of the parsing function + parseResult: [[], [], []], + }; +} + +module.exports = [testName, linter, extensions, getLintParams, getFixParams]; diff --git a/test/linters/projects/swiftformat/file1.swift b/test/linters/projects/swiftformat/file1.swift new file mode 100644 index 00000000..6c43cceb --- /dev/null +++ b/test/linters/projects/swiftformat/file1.swift @@ -0,0 +1,10 @@ +let str = "world" + +// "consecutiveBlankLines" warning + + +func main() { + print("hello \(str)") // "indent" warning +} + +main() diff --git a/test/linters/projects/swiftformat/file2.swift b/test/linters/projects/swiftformat/file2.swift new file mode 100644 index 00000000..b63fc913 --- /dev/null +++ b/test/linters/projects/swiftformat/file2.swift @@ -0,0 +1,2 @@ +// "semicolons" warning +print("hello \(str)");