├── .nvmrc ├── .gitignore ├── test ├── linters │ ├── projects │ │ ├── erblint │ │ │ ├── .gitignore │ │ │ ├── file1.erb │ │ │ ├── file2.erb │ │ │ ├── Gemfile │ │ │ ├── .rubocop.yml │ │ │ ├── .erb-lint.yml │ │ │ └── Gemfile.lock │ │ ├── rubocop │ │ │ ├── .gitignore │ │ │ ├── file2.rb │ │ │ ├── Gemfile │ │ │ ├── file1.rb │ │ │ ├── .rubocop.yml │ │ │ └── Gemfile.lock │ │ ├── xo │ │ │ ├── .gitignore │ │ │ ├── file2.js │ │ │ ├── package.json │ │ │ └── file1.js │ │ ├── eslint │ │ │ ├── .gitignore │ │ │ ├── file2.js │ │ │ ├── package.json │ │ │ ├── file1.js │ │ │ └── .eslintrc.json │ │ ├── tsc │ │ │ ├── .gitignore │ │ │ ├── file2.ts │ │ │ ├── package.json │ │ │ ├── file1.ts │ │ │ └── yarn.lock │ │ ├── black │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── flake8 │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── mypy │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── oitnb │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── prettier │ │ │ ├── .gitignore │ │ │ ├── file2.css │ │ │ ├── .prettierrc.json │ │ │ ├── file1.js │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── stylelint │ │ │ ├── .gitignore │ │ │ ├── file2.scss │ │ │ ├── file1.css │ │ │ ├── .stylelintrc.json │ │ │ └── package.json │ │ ├── autopep8 │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── clang-format │ │ │ ├── .clang-format │ │ │ ├── file1.c │ │ │ └── file2.mm │ │ ├── eslint-typescript │ │ │ ├── .gitignore │ │ │ ├── file2.js │ │ │ ├── file1.ts │ │ │ ├── .eslintrc.json │ │ │ └── package.json │ │ ├── pylint │ │ │ ├── requirements.txt │ │ │ ├── file2.py │ │ │ └── file1.py │ │ ├── swiftlint │ │ │ ├── Mintfile │ │ │ ├── .swiftlint.yml │ │ │ ├── file2.swift │ │ │ └── file1.swift │ │ ├── swift-format-lockwood │ │ │ ├── .swiftformat │ │ │ ├── Mintfile │ │ │ ├── file2.swift │ │ │ └── file1.swift │ │ ├── clippy │ │ │ ├── src │ │ │ │ ├── file1.rs │ │ │ │ └── main.rs │ │ │ ├── Cargo.lock │ │ │ └── Cargo.toml │ │ ├── swift-format-official │ │ │ ├── file2.swift │ │ │ └── file1.swift │ │ ├── rustfmt │ │ │ ├── src │ │ │ │ ├── main.rs │ │ │ │ └── foo.rs │ │ │ ├── Cargo.toml │ │ │ └── .gitignore │ │ ├── gofmt │ │ │ ├── file2.go │ │ │ └── file1.go │ │ ├── golint │ │ │ ├── file2.go │ │ │ └── file1.go │ │ ├── php-codesniffer │ │ │ ├── file1.php │ │ │ ├── phpcs.xml.dist │ │ │ └── file2.php │ │ └── dotnet-format │ │ │ ├── file1.cs │ │ │ ├── dotnet-format.csproj │ │ │ └── file2.cs │ ├── params │ │ ├── golint.js │ │ ├── mypy.js │ │ ├── flake8.js │ │ ├── tsc.js │ │ ├── prettier.js │ │ ├── clang-format.js │ │ ├── autopep8.js │ │ ├── erblint.js │ │ ├── dotnet-format.js │ │ ├── swift-format-official.js │ │ ├── black.js │ │ ├── swift-format-lockwood.js │ │ ├── gofmt.js │ │ ├── oitnb.js │ │ ├── swiftlint.js │ │ ├── rustfmt.js │ │ ├── stylelint.js │ │ ├── php-codesniffer.js │ │ ├── pylint.js │ │ ├── rubocop.js │ │ ├── eslint.js │ │ └── eslint-typescript.js │ └── linters.test.js ├── mock-actions-core.js ├── utils │ ├── command-exists.test.js │ └── npm │ │ ├── get-npm-bin-command.test.js │ │ └── use-yarn.test.js ├── github │ ├── test-constants.js │ ├── api.test.js │ └── api-responses │ │ └── check-runs.json └── test-utils.js ├── .github ├── screenshots │ ├── auto-fix.png │ ├── check-runs.png │ └── check-annotations.png ├── workflows │ ├── lock.yml │ ├── versioning.yml │ ├── lint.yml │ ├── codeql-analysis.yml │ ├── build.yml │ ├── stale.yml │ └── test-action.yml └── dependabot.yml ├── .gitattributes ├── .prettierignore ├── .editorconfig ├── src ├── utils │ ├── npm │ │ ├── get-npm-bin-command.js │ │ └── use-yarn.js │ ├── command-exists.js │ ├── string.js │ ├── diff.js │ ├── request.js │ ├── lint-result.js │ └── action.js ├── linters │ ├── index.js │ ├── xo.js │ ├── black.js │ ├── oitnb.js │ ├── swiftlint.js │ ├── autopep8.js │ ├── golint.js │ ├── rustfmt.js │ ├── swift-format-lockwood.js │ ├── prettier.js │ ├── clang-format.js │ ├── dotnet-format.js │ ├── swift-format-official.js │ ├── flake8.js │ ├── pylint.js │ ├── stylelint.js │ ├── erblint.js │ ├── gofmt.js │ ├── php-codesniffer.js │ ├── tsc.js │ ├── rubocop.js │ ├── eslint.js │ ├── mypy.js │ └── clippy.js ├── git.js └── github │ ├── api.js │ └── context.js ├── LICENSE.md ├── CREDITS.md ├── CONTRIBUTING.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/rubocop/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/xo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/eslint/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/tsc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/black/requirements.txt: -------------------------------------------------------------------------------- 1 | black>=19.10b0 2 | -------------------------------------------------------------------------------- /test/linters/projects/flake8/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.8.4 2 | -------------------------------------------------------------------------------- /test/linters/projects/mypy/requirements.txt: -------------------------------------------------------------------------------- 1 | mypy>=0.761 2 | -------------------------------------------------------------------------------- /test/linters/projects/oitnb/requirements.txt: -------------------------------------------------------------------------------- 1 | oitnb>=0.2.2 2 | -------------------------------------------------------------------------------- /test/linters/projects/prettier/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/stylelint/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/autopep8/requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8>=1.5.0 2 | -------------------------------------------------------------------------------- /test/linters/projects/clang-format/.clang-format: -------------------------------------------------------------------------------- 1 | UseTab: Never 2 | -------------------------------------------------------------------------------- /test/linters/projects/clang-format/file1.c: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /test/linters/projects/eslint-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/linters/projects/pylint/requirements.txt: -------------------------------------------------------------------------------- 1 | pylint==2.14.5 2 | -------------------------------------------------------------------------------- /test/linters/projects/swiftlint/Mintfile: -------------------------------------------------------------------------------- 1 | realm/SwiftLint@0.50.3 2 | -------------------------------------------------------------------------------- /test/linters/projects/clang-format/file2.mm: -------------------------------------------------------------------------------- 1 | @interface Foo : NSObject @end 2 | -------------------------------------------------------------------------------- /test/linters/projects/swiftlint/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | trailing_semicolon: error 2 | -------------------------------------------------------------------------------- /test/linters/projects/stylelint/file2.scss: -------------------------------------------------------------------------------- 1 | main {} // "block-no-empty" error 2 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-lockwood/.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.1 2 | -------------------------------------------------------------------------------- /test/linters/projects/prettier/file2.css: -------------------------------------------------------------------------------- 1 | body {color: red;} /* Line break errors */ 2 | -------------------------------------------------------------------------------- /test/linters/projects/pylint/file2.py: -------------------------------------------------------------------------------- 1 | a = 5 2 | b = 7 3 | c = a + b 4 | print(c) 5 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-lockwood/Mintfile: -------------------------------------------------------------------------------- 1 | nicklockwood/SwiftFormat@0.48.18 2 | -------------------------------------------------------------------------------- /test/linters/projects/xo/file2.js: -------------------------------------------------------------------------------- 1 | const str = 'Hello world'; // "no-unused-vars" error 2 | -------------------------------------------------------------------------------- /test/linters/projects/eslint/file2.js: -------------------------------------------------------------------------------- 1 | const str = 'Hello world'; // "no-unused-vars" error 2 | -------------------------------------------------------------------------------- /test/linters/projects/eslint-typescript/file2.js: -------------------------------------------------------------------------------- 1 | const str = 'Hello world'; // "no-unused-vars" error 2 | -------------------------------------------------------------------------------- /test/linters/projects/swiftlint/file2.swift: -------------------------------------------------------------------------------- 1 | // "trailing_semicolon" error 2 | print("hello \(str)"); 3 | -------------------------------------------------------------------------------- /test/linters/projects/black/file2.py: -------------------------------------------------------------------------------- 1 | def add(num_1, num_2): 2 | return num_1 + num_2 # Indentation error 3 | -------------------------------------------------------------------------------- /test/linters/projects/clippy/src/file1.rs: -------------------------------------------------------------------------------- 1 | pub fn sayHi(name: &str) { 2 | print!("Hello, {} !", name); 3 | } -------------------------------------------------------------------------------- /test/linters/projects/erblint/file1.erb: -------------------------------------------------------------------------------- 1 | <% for @item in @shopping_list %> 2 | <%= @item %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/linters/projects/flake8/file2.py: -------------------------------------------------------------------------------- 1 | def add(num_1, num_2): 2 | return num_1 + num_2 # Indentation error 3 | -------------------------------------------------------------------------------- /test/linters/projects/oitnb/file2.py: -------------------------------------------------------------------------------- 1 | def add(num_1, num_2): 2 | return num_1 + num_2 # Indentation error 3 | -------------------------------------------------------------------------------- /test/linters/projects/stylelint/file1.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red;; /* "no-extra-semicolons" warning */ 3 | } 4 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-lockwood/file2.swift: -------------------------------------------------------------------------------- 1 | // "semicolons" warning 2 | print("hello \(str)"); 3 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-official/file2.swift: -------------------------------------------------------------------------------- 1 | // "semicolons" warning 2 | print("hello \(str)"); 3 | -------------------------------------------------------------------------------- /test/linters/projects/autopep8/file2.py: -------------------------------------------------------------------------------- 1 | def add(num_1, num_2): 2 | return num_1 + num_2 # Indentation error 3 | -------------------------------------------------------------------------------- /test/linters/projects/pylint/file1.py: -------------------------------------------------------------------------------- 1 | class animal: 2 | def __init__(self, name): 3 | self.name = name 4 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/file2.erb: -------------------------------------------------------------------------------- 1 | Hello, <%= @name %>. 2 |
3 | Today is <%= Time.now.strftime('%A') %>. 4 | -------------------------------------------------------------------------------- /.github/screenshots/auto-fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/lint-action/HEAD/.github/screenshots/auto-fix.png -------------------------------------------------------------------------------- /test/linters/projects/prettier/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "useTabs": true 5 | } 6 | -------------------------------------------------------------------------------- /test/linters/projects/rubocop/file2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # "Lint/UselessAssignment" warning 4 | x = 1 5 | -------------------------------------------------------------------------------- /.github/screenshots/check-runs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/lint-action/HEAD/.github/screenshots/check-runs.png -------------------------------------------------------------------------------- /test/linters/projects/mypy/file2.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | 4 | def helper(var: Mapping[str, str]): 5 | pass 6 | -------------------------------------------------------------------------------- /test/linters/projects/rustfmt/src/main.rs: -------------------------------------------------------------------------------- 1 | mod foo; 2 | fn main() {let delta = foo::delta(); println!("Time delta is {delta:?}");} 3 | -------------------------------------------------------------------------------- /test/linters/projects/tsc/file2.ts: -------------------------------------------------------------------------------- 1 | let num = 1; 2 | 3 | num = 'hello'; //Type 'string' is not assignable to type 'number'.ts(2322) 4 | -------------------------------------------------------------------------------- /.github/screenshots/check-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/lint-action/HEAD/.github/screenshots/check-annotations.png -------------------------------------------------------------------------------- /test/linters/projects/rubocop/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rubocop', '~> 0.93.0' 6 | -------------------------------------------------------------------------------- /test/linters/projects/gofmt/file2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func divide(num1 int, num2 int) int { 4 | return num1 / num2 // Whitespace error 5 | } 6 | -------------------------------------------------------------------------------- /test/linters/projects/prettier/file1.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | console.log("Hello world"); // "singleQuote" error 3 | } 4 | 5 | main() // "semi" error 6 | -------------------------------------------------------------------------------- /test/linters/projects/clippy/src/main.rs: -------------------------------------------------------------------------------- 1 | mod file1; 2 | use file1::sayHi; 3 | 4 | fn main() { 5 | println!("Hello, world!"); 6 | 7 | sayHi("clippy"); 8 | } -------------------------------------------------------------------------------- /test/linters/projects/rubocop/file1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def method 4 | # "Style/RedundantReturn" convention 5 | return 'words' 6 | end 7 | -------------------------------------------------------------------------------- /test/linters/projects/golint/file2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Error for incorrect comment format 4 | func Divide(num1 int, num2 int) int { 5 | return num1 / num2 6 | } 7 | -------------------------------------------------------------------------------- /test/linters/projects/stylelint/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-no-empty": true, 4 | "no-extra-semicolons": [true, { "severity": "warning" }] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/linters/projects/xo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-xo", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "xo": "^0.53.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | # Exclude test and build files in project's language stats 4 | dist/* linguist-generated 5 | test/linters/projects/** linguist-vendored 6 | -------------------------------------------------------------------------------- /test/linters/projects/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-eslint", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "eslint": "^8.31.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/linters/projects/tsc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-eslint", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "typescript": "^4.9.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # TODO: Move config into `package.json` once supported 2 | # https://github.com/prettier/prettier/issues/3460 3 | 4 | node_modules/ 5 | test/linters/projects/ 6 | dist/ 7 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rubocop', '~> 1.12.0' 6 | gem 'erb_lint', '~> 0.1.1', require: false 7 | -------------------------------------------------------------------------------- /test/linters/projects/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-prettier", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "prettier": "^2.8.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/linters/projects/swiftlint/file1.swift: -------------------------------------------------------------------------------- 1 | let str = "world" 2 | 3 | // "vertical_whitespace" warning 4 | 5 | 6 | func main() { 7 | print("hello \(str)") 8 | } 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /test/linters/projects/clippy/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "lint" 5 | version = "0.0.1" 6 | 7 | -------------------------------------------------------------------------------- /test/linters/projects/php-codesniffer/file1.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | . 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/linters/projects/xo/file1.js: -------------------------------------------------------------------------------- 1 | let str = 'world'; // "prefer-const" warning 2 | 3 | function main() { 4 | // "no-warning-comments" error 5 | console.log('hello ' + str); // TODO: Change something 6 | } 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /test/linters/projects/eslint/file1.js: -------------------------------------------------------------------------------- 1 | let str = 'world'; // "prefer-const" warning 2 | 3 | function main() { 4 | // "no-warning-comments" error 5 | console.log('hello ' + str); // TODO: Change something 6 | } 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /test/linters/projects/mypy/file1.py: -------------------------------------------------------------------------------- 1 | from file2 import helper 2 | 3 | 4 | def main(input_str: str): 5 | print(input_str) 6 | print(helper({ 7 | input_str: 42, 8 | })) 9 | 10 | 11 | main(["hello"]) 12 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-lockwood/file1.swift: -------------------------------------------------------------------------------- 1 | let str = "world" 2 | 3 | // "consecutiveBlankLines" warning 4 | 5 | 6 | func main() { 7 | print("hello \(str)") // "indent" warning 8 | } 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /test/linters/projects/swift-format-official/file1.swift: -------------------------------------------------------------------------------- 1 | let str = "world" 2 | 3 | // "consecutiveBlankLines" warning 4 | 5 | 6 | func main() { 7 | print("hello \(str)") // "indent" warning 8 | } 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /test/linters/projects/dotnet-format/file1.cs: -------------------------------------------------------------------------------- 1 | namespace dotnet.format; // IDE0073: A source file is missing a required header. 2 | 3 | class file1 4 | { 5 | static void Main() 6 | { 7 | file2.Test(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/linters/projects/eslint-typescript/file1.ts: -------------------------------------------------------------------------------- 1 | let str = 'world'; // "prefer-const" warning 2 | 3 | function main(): void { 4 | // "no-warning-comments" error 5 | console.log('hello ' + str); // TODO: Change something 6 | } 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /test/linters/projects/eslint/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "no-unused-vars": 2, 9 | "no-warning-comments": 1, 10 | "prefer-const": 2 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/linters/projects/clippy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lint" 3 | version = "0.0.1" 4 | authors = ["lint "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | -------------------------------------------------------------------------------- /test/linters/projects/rustfmt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-fmt-test-project" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_mode: 2 | merge: 3 | - Exclude 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.4 7 | NewCops: enable 8 | Exclude: 9 | - vendor/bundle/**/* 10 | 11 | Layout/EndOfLine: 12 | Enabled: false 13 | -------------------------------------------------------------------------------- /test/linters/projects/rubocop/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_mode: 2 | merge: 3 | - Exclude 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.4 7 | NewCops: enable 8 | Exclude: 9 | - vendor/bundle/**/* 10 | 11 | Layout/EndOfLine: 12 | Enabled: false 13 | -------------------------------------------------------------------------------- /test/linters/projects/php-codesniffer/file2.php: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /test/linters/projects/eslint-typescript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "es6": true 7 | }, 8 | "rules": { 9 | "no-unused-vars": 2, 10 | "no-warning-comments": 1, 11 | "prefer-const": 2 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yaml,yml}] 12 | indent_style = space 13 | 14 | [*.py] 15 | indent_style = space 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /test/linters/projects/eslint-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-eslint-typescript", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "@typescript-eslint/eslint-plugin": "^5.48.0", 7 | "@typescript-eslint/parser": "^5.48.0", 8 | "eslint": "^8.31.0", 9 | "typescript": "^4.9.4" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/linters/projects/tsc/file1.ts: -------------------------------------------------------------------------------- 1 | let str; // Variable 'str' implicitly has type 'any' in some locations where its type cannot be determined.ts(7034) 2 | 3 | function main(): void { 4 | console.log('hello ' + str); // Variable 'str' implicitly has type 'any' in some locations where its type cannot be determined.ts(7034) 5 | } 6 | 7 | main(); 8 | -------------------------------------------------------------------------------- /test/linters/projects/tsc/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^4.9.4: 6 | version "4.9.4" 7 | resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" 8 | integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== 9 | -------------------------------------------------------------------------------- /test/linters/projects/golint/file1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | var str = "world" 6 | 7 | func main() { 8 | fmt.Println("hello " + doSomething(str)) 9 | } 10 | 11 | func doSomething(str string) string { 12 | if str == "" { 13 | return "default" 14 | } else { 15 | // Error for unnecessary "else" statement 16 | return str 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/linters/projects/rustfmt/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Ignoring the lock file since this is a test project for the lint action to 6 | # test against 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: Lock closed issues and pull requests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "45 4 * * *" 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: dessant/lock-threads@v5 14 | with: 15 | issue-inactive-days: 10 16 | pr-inactive-days: 10 17 | -------------------------------------------------------------------------------- /test/linters/projects/rustfmt/src/foo.rs: -------------------------------------------------------------------------------- 1 | //! A big doc comment to force the start line to be something other than "1" 2 | //! 3 | //! 4 | //! This should push the error start line down past 1 5 | use std::time::{SystemTime, Duration}; 6 | 7 | pub fn delta() -> Duration { 8 | let start = SystemTime::now(); let delta = start.elapsed().unwrap(); 9 | delta 10 | } 11 | -------------------------------------------------------------------------------- /test/mock-actions-core.js: -------------------------------------------------------------------------------- 1 | // Disable logging utilities in @actions/core. 2 | jest.mock("@actions/core", () => { 3 | const original = jest.requireActual("@actions/core"); 4 | return { 5 | ...original, 6 | debug: jest.fn(), 7 | error: jest.fn(), 8 | warning: jest.fn(), 9 | info: jest.fn(), 10 | startGroup: jest.fn(), 11 | endGroup: jest.fn(), 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /test/linters/projects/dotnet-format/dotnet-format.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | dotnet_format 7 | disable 8 | enable 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | name: Keep the major version tag up-to-date 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | update-major-tag: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: Actions-R-Us/actions-tagger@v2 17 | -------------------------------------------------------------------------------- /test/linters/projects/prettier/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | prettier@^2.8.1: 6 | version "2.8.1" 7 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" 8 | integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== 9 | -------------------------------------------------------------------------------- /test/linters/projects/gofmt/file1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | var str = "world" 6 | 7 | func main () { // Whitespace error 8 | fmt.Println("hello " + str) 9 | } 10 | 11 | func add(num1 int, num2 int) int { 12 | return num1 + num2 13 | } 14 | 15 | func subtract(num1 int, num2 int) int { 16 | return num1 - num2 17 | } 18 | 19 | func multiply(num1 int, num2 int) int { 20 | return num1 * num2 // Indentation error 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | time: "10:00" 8 | timezone: "Europe/Zurich" 9 | open-pull-requests-limit: 10 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | time: "10:00" 15 | timezone: "Europe/Zurich" 16 | open-pull-requests-limit: 10 17 | -------------------------------------------------------------------------------- /test/linters/projects/stylelint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-stylelint", 3 | "main": "index.js", 4 | "private": true, 5 | "devDependencies": { 6 | "postcss": "^8.4.20", 7 | "stylelint": "^14.16.1", 8 | "stylelint-config-standard-scss": "^6.1.0" 9 | }, 10 | "stylelint": { 11 | "extends": "stylelint-config-standard-scss", 12 | "rules": { 13 | "no-extra-semicolons": [ true, { 14 | "severity": "warning" 15 | } ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/utils/command-exists.test.js: -------------------------------------------------------------------------------- 1 | const commandExists = require("../../src/utils/command-exists"); 2 | 3 | describe("commandExists()", () => { 4 | test("should return `true` for existing command", async () => { 5 | await expect(commandExists("cat")).resolves.toEqual(true); 6 | }); 7 | 8 | test("should return `false` for non-existent command", async () => { 9 | await expect(commandExists("nonexistentcommand")).resolves.toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/linters/projects/autopep8/file1.py: -------------------------------------------------------------------------------- 1 | var_1 = "hello" 2 | var_2 = "world" 3 | 4 | 5 | def main (): # Whitespace error 6 | print("hello " + var_2) 7 | 8 | 9 | def add(num_1, num_2): 10 | return num_1 + num_2 11 | 12 | 13 | def subtract(num_1, num_2): 14 | return num_1 - num_2 15 | 16 | 17 | def multiply(num_1, num_2): 18 | return num_1 * num_2 19 | 20 | 21 | def divide(num_1, num_2): 22 | return num_1 / num_2 23 | 24 | # Blank lines error 25 | 26 | main() 27 | -------------------------------------------------------------------------------- /test/linters/projects/black/file1.py: -------------------------------------------------------------------------------- 1 | var_1 = "hello" 2 | var_2 = "world" 3 | 4 | 5 | def main (): # Whitespace error 6 | print("hello " + var_2) 7 | 8 | 9 | def add(num_1, num_2): 10 | return num_1 + num_2 11 | 12 | 13 | def subtract(num_1, num_2): 14 | return num_1 - num_2 15 | 16 | 17 | def multiply(num_1, num_2): 18 | return num_1 * num_2 19 | 20 | 21 | def divide(num_1, num_2): 22 | return num_1 / num_2 23 | 24 | # Blank lines error 25 | 26 | main() 27 | -------------------------------------------------------------------------------- /test/linters/projects/flake8/file1.py: -------------------------------------------------------------------------------- 1 | var_1 = "hello" 2 | var_2 = "world" 3 | 4 | 5 | def main (): # Whitespace error 6 | print("hello " + var_2) 7 | 8 | 9 | def add(num_1, num_2): 10 | return num_1 + num_2 11 | 12 | 13 | def subtract(num_1, num_2): 14 | return num_1 - num_2 15 | 16 | 17 | def multiply(num_1, num_2): 18 | return num_1 * num_2 19 | 20 | 21 | def divide(num_1, num_2): 22 | return num_1 / num_2 23 | 24 | # Blank lines error 25 | 26 | main() 27 | -------------------------------------------------------------------------------- /test/linters/projects/oitnb/file1.py: -------------------------------------------------------------------------------- 1 | var_1 = "hello" 2 | var_2 = "world" 3 | 4 | 5 | def main (): # Whitespace error 6 | print("hello " + var_2) 7 | 8 | 9 | def add(num_1, num_2): 10 | return num_1 + num_2 11 | 12 | 13 | def subtract(num_1, num_2): 14 | return num_1 - num_2 15 | 16 | 17 | def multiply(num_1, num_2): 18 | return num_1 * num_2 19 | 20 | 21 | def divide(num_1, num_2): 22 | return num_1 / num_2 23 | 24 | # Blank lines error 25 | 26 | main() 27 | -------------------------------------------------------------------------------- /src/utils/npm/get-npm-bin-command.js: -------------------------------------------------------------------------------- 1 | const { useYarn } = require("./use-yarn"); 2 | 3 | /** 4 | * Returns the NPM or Yarn command ({@see useYarn()}) for executing an NPM binary 5 | * @param {string} [pkgRoot] - Package directory (directory where Yarn lockfile would exist) 6 | * @returns {string} - NPM/Yarn command for executing the NPM binary. The binary name should be 7 | * appended to this command 8 | */ 9 | function getNpmBinCommand(pkgRoot) { 10 | return useYarn(pkgRoot) ? "yarn run --silent" : "npx --no-install"; 11 | } 12 | 13 | module.exports = { getNpmBinCommand }; 14 | -------------------------------------------------------------------------------- /test/linters/projects/dotnet-format/file2.cs: -------------------------------------------------------------------------------- 1 | // Test header. 2 | 3 | using System.Collections.Generic; // IMPORTS: Fix imports ordering 4 | using System; 5 | 6 | namespace dotnet.format; 7 | 8 | class file2 9 | { 10 | public static void Test() 11 | { 12 | var myList = new List() 13 | { 14 | "Hello", 15 | "World" 16 | }; 17 | 18 | foreach (var item in myList) 19 | { 20 | Console.WriteLine( item); // WHITESPACE: Fix whitespace formatting. Delete 1 characters. 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/.erb-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | EnableDefaultLinters: true 3 | linters: 4 | Rubocop: 5 | enabled: true 6 | rubocop_config: 7 | inherit_from: 8 | - .rubocop.yml 9 | Layout/InitialIndentation: 10 | Enabled: false 11 | Layout/LineLength: 12 | Enabled: false 13 | Layout/TrailingEmptyLines: 14 | Enabled: false 15 | Layout/TrailingWhitespace: 16 | Enabled: false 17 | Naming/FileName: 18 | Enabled: false 19 | Style/FrozenStringLiteralComment: 20 | Enabled: false 21 | Lint/UselessAssignment: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /src/utils/command-exists.js: -------------------------------------------------------------------------------- 1 | const checkForCommand = require("command-exists"); 2 | 3 | /** 4 | * Returns whether the provided shell command is available 5 | * @param {string} command - Shell command to check for 6 | * @returns {Promise} - Whether the command is available 7 | */ 8 | async function commandExists(command) { 9 | // The `command-exists` library throws an error if the command is not available. This function 10 | // catches these errors and returns a boolean value instead 11 | try { 12 | await checkForCommand(command); 13 | return true; 14 | } catch (error) { 15 | return false; 16 | } 17 | } 18 | 19 | module.exports = commandExists; 20 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalizes the first letter of a string 3 | * @param {string} str - String to process 4 | * @returns {string} - Input string with first letter capitalized 5 | */ 6 | function capitalizeFirstLetter(str) { 7 | return str.charAt(0).toUpperCase() + str.slice(1); 8 | } 9 | 10 | /** 11 | * Removes the trailing period from the provided string (if it has one) 12 | * @param {string} str - String to process 13 | * @returns {string} - String without trailing period 14 | */ 15 | function removeTrailingPeriod(str) { 16 | return str[str.length - 1] === "." ? str.substring(0, str.length - 1) : str; 17 | } 18 | 19 | module.exports = { 20 | capitalizeFirstLetter, 21 | removeTrailingPeriod, 22 | }; 23 | -------------------------------------------------------------------------------- /test/github/test-constants.js: -------------------------------------------------------------------------------- 1 | const USERNAME = "test-user"; 2 | const REPOSITORY_NAME = "test-repo"; 3 | const REPOSITORY = `${USERNAME}/${REPOSITORY_NAME}`; 4 | 5 | const FORK_USERNAME = "fork-user"; 6 | const FORK_REPOSITORY_NAME = `${REPOSITORY_NAME}-fork`; 7 | const FORK_REPOSITORY = `${FORK_USERNAME}/${FORK_REPOSITORY_NAME}`; 8 | 9 | const BRANCH = "test-branch"; 10 | const REPOSITORY_DIR = "/path/to/cloned/repo"; 11 | const TOKEN = "test-token"; 12 | 13 | const EVENT_NAME = "push"; 14 | const EVENT_PATH = "/path/to/event.json"; 15 | 16 | module.exports = { 17 | USERNAME, 18 | REPOSITORY_NAME, 19 | REPOSITORY, 20 | FORK_USERNAME, 21 | FORK_REPOSITORY_NAME, 22 | FORK_REPOSITORY, 23 | BRANCH, 24 | REPOSITORY_DIR, 25 | TOKEN, 26 | EVENT_NAME, 27 | EVENT_PATH, 28 | }; 29 | -------------------------------------------------------------------------------- /test/utils/npm/get-npm-bin-command.test.js: -------------------------------------------------------------------------------- 1 | const { getNpmBinCommand } = require("../../../src/utils/npm/get-npm-bin-command"); 2 | const { useYarn } = require("../../../src/utils/npm/use-yarn"); 3 | 4 | jest.mock("../../../src/utils/action"); 5 | jest.mock("../../../src/utils/npm/use-yarn"); 6 | 7 | describe("runNpmBin()", () => { 8 | test("should run correct Yarn command", () => { 9 | useYarn.mockReturnValue(true); 10 | const npmBinCommand = getNpmBinCommand("/this/path/is/not/used"); 11 | expect(npmBinCommand).toEqual("yarn run --silent"); 12 | }); 13 | 14 | test("should run correct NPM command", () => { 15 | useYarn.mockReturnValue(false); 16 | const npmBinCommand = getNpmBinCommand("/this/path/is/not/used"); 17 | expect(npmBinCommand).toEqual("npx --no-install"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/linters/projects/rubocop/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | parallel (1.22.1) 6 | parser (3.1.3.0) 7 | ast (~> 2.4.1) 8 | rainbow (3.1.1) 9 | regexp_parser (2.6.1) 10 | rexml (3.2.5) 11 | rubocop (0.93.1) 12 | parallel (~> 1.10) 13 | parser (>= 2.7.1.5) 14 | rainbow (>= 2.2.2, < 4.0) 15 | regexp_parser (>= 1.8) 16 | rexml 17 | rubocop-ast (>= 0.6.0) 18 | ruby-progressbar (~> 1.7) 19 | unicode-display_width (>= 1.4.0, < 2.0) 20 | rubocop-ast (1.24.1) 21 | parser (>= 3.1.1.0) 22 | ruby-progressbar (1.11.0) 23 | unicode-display_width (1.8.0) 24 | 25 | PLATFORMS 26 | ruby 27 | 28 | DEPENDENCIES 29 | rubocop (~> 0.93.0) 30 | 31 | BUNDLED WITH 32 | 2.1.4 33 | -------------------------------------------------------------------------------- /src/utils/diff.js: -------------------------------------------------------------------------------- 1 | const parseDiff = require("parse-diff"); 2 | 3 | /** 4 | * Parses linting errors from a unified diff 5 | * @param {string} diff - Unified diff 6 | * @returns {{path: string, firstLine: number, lastLine: number, message: string}[]} - Array of 7 | * parsed errors 8 | */ 9 | function parseErrorsFromDiff(diff) { 10 | const errors = []; 11 | const files = parseDiff(diff); 12 | for (const file of files) { 13 | const { chunks, to: path } = file; 14 | for (const chunk of chunks) { 15 | const { oldStart, oldLines, changes } = chunk; 16 | const chunkDiff = changes.map((change) => change.content).join("\n"); 17 | errors.push({ 18 | path, 19 | firstLine: oldStart, 20 | lastLine: oldStart + oldLines, 21 | message: chunkDiff, 22 | }); 23 | } 24 | } 25 | return errors; 26 | } 27 | 28 | module.exports = { 29 | parseErrorsFromDiff, 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/npm/use-yarn.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs"); 2 | const { join } = require("path"); 3 | 4 | const YARN_LOCK_NAME = "yarn.lock"; 5 | 6 | /** 7 | * Determines whether Yarn should be used to execute commands or binaries. This decision is based on 8 | * the existence of a Yarn lockfile in the package directory. The distinction between NPM and Yarn 9 | * is necessary e.g. for Yarn Plug'n'Play to work 10 | * @param {string} [pkgRoot] - Package directory (directory where Yarn lockfile would exist) 11 | * @returns {boolean} - Whether Yarn should be used 12 | */ 13 | function useYarn(pkgRoot) { 14 | // Use an absolute path if `pkgRoot` is specified and a relative one (current directory) otherwise 15 | const lockfilePath = pkgRoot ? join(pkgRoot, YARN_LOCK_NAME) : YARN_LOCK_NAME; 16 | return existsSync(lockfilePath); 17 | } 18 | 19 | module.exports = { useYarn }; 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | lint: 20 | name: Lint 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Check out Git repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version-file: ".nvmrc" 31 | cache: "yarn" 32 | 33 | - name: Install dependencies 34 | run: yarn install 35 | 36 | - name: Lint 37 | run: | 38 | yarn lint 39 | yarn format 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: "36 15 * * 6" 12 | 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | language: ["javascript"] 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | const https = require("https"); 2 | 3 | /** 4 | * Helper function for making HTTP requests 5 | * @param {string | URL} url - Request URL 6 | * @param {object} options - Request options 7 | * @returns {Promise} - JSON response 8 | */ 9 | function request(url, options) { 10 | return new Promise((resolve, reject) => { 11 | const req = https 12 | .request(url, options, (res) => { 13 | let data = ""; 14 | res.on("data", (chunk) => { 15 | data += chunk; 16 | }); 17 | res.on("end", () => { 18 | if (res.statusCode >= 400) { 19 | const err = new Error(`Received status code ${res.statusCode}`); 20 | err.response = res; 21 | err.data = data; 22 | reject(err); 23 | } else { 24 | resolve({ res, data: JSON.parse(data) }); 25 | } 26 | }); 27 | }) 28 | .on("error", reject); 29 | if (options.body) { 30 | req.end(JSON.stringify(options.body)); 31 | } else { 32 | req.end(); 33 | } 34 | }); 35 | } 36 | 37 | module.exports = request; 38 | -------------------------------------------------------------------------------- /test/utils/npm/use-yarn.test.js: -------------------------------------------------------------------------------- 1 | const { mkdir } = require("fs").promises; 2 | const { join } = require("path"); 3 | 4 | const { ensureFile, remove } = require("fs-extra"); 5 | 6 | const { useYarn } = require("../../../src/utils/npm/use-yarn"); 7 | const { createTmpDir } = require("../../test-utils"); 8 | 9 | const tmpDir = createTmpDir(); 10 | 11 | afterAll(async () => { 12 | await remove(tmpDir); 13 | }); 14 | 15 | describe("useYarn()", () => { 16 | test("should return `true` if there is a Yarn lockfile", async () => { 17 | const dir = join(tmpDir, "yarn-project"); 18 | const lockfilePath = join(dir, "yarn.lock"); 19 | 20 | // Create temp directory with lockfile 21 | await mkdir(dir); 22 | await ensureFile(lockfilePath); 23 | 24 | expect(useYarn(dir)).toBeTruthy(); 25 | }); 26 | 27 | test("should return `false` if there is no Yarn lockfile", async () => { 28 | const dir = join(tmpDir, "npm-project"); 29 | 30 | // Create temp directory without lockfile 31 | await mkdir(dir); 32 | 33 | expect(useYarn(dir)).toBeFalsy(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Samuel Meuli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Check out Git repository 22 | uses: actions/checkout@v4 23 | with: 24 | # Custom token to allow commits trigger other workflows. 25 | token: ${{ secrets.BUILD_ACTION_GITHUB_TOKEN }} 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version-file: ".nvmrc" 31 | cache: "yarn" 32 | 33 | - name: Install dependencies 34 | run: yarn install 35 | 36 | - name: Build dist 37 | run: yarn build 38 | 39 | - name: Commit dist 40 | uses: EndBug/add-and-commit@v9 41 | with: 42 | add: "dist" 43 | author_name: github-actions[bot] 44 | author_email: github-actions[bot]@users.noreply.github.com 45 | message: "[auto] Update compiled version" 46 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "45 2 * * *" 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/stale@v9 18 | with: 19 | stale-issue-message: | 20 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. 21 | stale-issue-label: "stale" 22 | exempt-issue-labels: "bug,blocked,in progress,never-stale" 23 | days-before-issue-stale: 60 24 | days-before-issue-close: 20 25 | stale-pr-message: | 26 | Thank you for your contribution to this project! 27 | 28 | This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. 29 | stale-pr-label: "stale" 30 | exempt-pr-labels: "waiting for review,never-stale" 31 | days-before-pr-stale: 90 32 | days-before-pr-close: 20 33 | -------------------------------------------------------------------------------- /test/github/api.test.js: -------------------------------------------------------------------------------- 1 | const { createCheck } = require("../../src/github/api"); 2 | const { 3 | EVENT_NAME, 4 | EVENT_PATH, 5 | FORK_REPOSITORY, 6 | REPOSITORY, 7 | REPOSITORY_DIR, 8 | TOKEN, 9 | USERNAME, 10 | } = require("./test-constants"); 11 | 12 | jest.mock("../../src/utils/request", () => 13 | // eslint-disable-next-line global-require 14 | jest.fn().mockReturnValue(require("./api-responses/check-runs.json")), 15 | ); 16 | 17 | describe("createCheck()", () => { 18 | const LINT_RESULT = { 19 | isSuccess: true, 20 | warning: [], 21 | error: [], 22 | }; 23 | const context = { 24 | actor: USERNAME, 25 | event: {}, 26 | eventName: EVENT_NAME, 27 | eventPath: EVENT_PATH, 28 | repository: { 29 | repoName: REPOSITORY, 30 | forkName: FORK_REPOSITORY, 31 | hasFork: false, 32 | }, 33 | token: TOKEN, 34 | workspace: REPOSITORY_DIR, 35 | }; 36 | 37 | test("mocked request should be successful", async () => { 38 | await expect( 39 | createCheck("check-name", "sha", context, LINT_RESULT, false, "summary"), 40 | ).resolves.toEqual(undefined); 41 | }); 42 | 43 | test("mocked request should fail when no lint results are provided", async () => { 44 | await expect(createCheck("check-name", "sha", context, null, false, "summary")).rejects.toEqual( 45 | expect.any(Error), 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/utils/lint-result.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lint result object. 3 | * @typedef LintResult 4 | * @property {boolean} isSuccess Whether the result is success. 5 | * @property {object[]} warning Warnings. 6 | * @property {object[]} error Errors. 7 | */ 8 | 9 | /** 10 | * Returns an object for storing linting results 11 | * @returns {LintResult} - Default object 12 | */ 13 | function initLintResult() { 14 | return { 15 | isSuccess: true, // Usually determined by the exit code of the linting command 16 | warning: [], 17 | error: [], 18 | }; 19 | } 20 | 21 | /** 22 | * Returns a text summary of the number of issues found when linting 23 | * @param {LintResult} lintResult - Parsed linter 24 | * output 25 | * @returns {string} - Text summary 26 | */ 27 | function getSummary(lintResult) { 28 | const nrErrors = lintResult.error.length; 29 | const nrWarnings = lintResult.warning.length; 30 | // Build and log a summary of linting errors/warnings 31 | if (nrWarnings > 0 && nrErrors > 0) { 32 | return `${nrErrors} error${nrErrors > 1 ? "s" : ""} and ${nrWarnings} warning${ 33 | nrWarnings > 1 ? "s" : "" 34 | }`; 35 | } 36 | if (nrErrors > 0) { 37 | return `${nrErrors} error${nrErrors > 1 ? "s" : ""}`; 38 | } 39 | if (nrWarnings > 0) { 40 | return `${nrWarnings} warning${nrWarnings > 1 ? "s" : ""}`; 41 | } 42 | return `no issues`; 43 | } 44 | 45 | module.exports = { 46 | getSummary, 47 | initLintResult, 48 | }; 49 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Source Code 4 | 5 | Parts of this action's code are inspired by the [ESLint Action](https://github.com/gimenete/eslint-action) by Alberto Gimeno. 6 | 7 | > MIT License 8 | > 9 | > Copyright (c) 2019 Alberto Gimeno 10 | > 11 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | ## Assets 18 | 19 | Lint Action GitHub profile picture: ["Clean free icon"](https://www.flaticon.com/free-icon/clean_2059802) designed by [Freepik](https://www.flaticon.com/authors/freepik) 20 | -------------------------------------------------------------------------------- /test/linters/params/golint.js: -------------------------------------------------------------------------------- 1 | const Golint = require("../../../src/linters/golint"); 2 | 3 | const testName = "golint"; 4 | const linter = Golint; 5 | const commandPrefix = ""; 6 | const args = ""; 7 | const extensions = ["go"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdoutFile1 = 12 | "file1.go:14:9: if block ends with a return statement, so drop this else and outdent its block"; 13 | const stdoutFile2 = 14 | 'file2.go:3:1: comment on exported function Divide should be of the form "Divide ..."'; 15 | return { 16 | // Expected output of the linting function 17 | cmdOutput: { 18 | status: 1, 19 | stdoutParts: [stdoutFile1, stdoutFile2], 20 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 21 | }, 22 | // Expected output of the parsing function 23 | lintResult: { 24 | isSuccess: false, 25 | warning: [], 26 | error: [ 27 | { 28 | path: "file1.go", 29 | firstLine: 14, 30 | lastLine: 14, 31 | message: `If block ends with a return statement, so drop this else and outdent its block`, 32 | }, 33 | { 34 | path: "file2.go", 35 | firstLine: 3, 36 | lastLine: 3, 37 | message: `Comment on exported function Divide should be of the form "Divide ..."`, 38 | }, 39 | ], 40 | }, 41 | }; 42 | } 43 | 44 | // Linting with auto-fixing 45 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 46 | 47 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 48 | -------------------------------------------------------------------------------- /test/linters/params/mypy.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | 3 | const Mypy = require("../../../src/linters/mypy"); 4 | 5 | const testName = "mypy"; 6 | const linter = Mypy; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["py"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const stdoutPart1 = `file1.py:7: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str"`; 14 | const stdoutPart2 = `file1.py:11: error: Argument 1 to "main" has incompatible type "List[str]"; expected "str"`; 15 | return { 16 | // Expected output of the linting function 17 | cmdOutput: { 18 | status: 1, 19 | stdoutParts: [stdoutPart1, stdoutPart2], 20 | stdout: `${stdoutPart1}${EOL}${stdoutPart2}`, 21 | }, 22 | // Expected output of the parsing function 23 | lintResult: { 24 | isSuccess: false, 25 | warning: [], 26 | error: [ 27 | { 28 | path: "file1.py", 29 | firstLine: 7, 30 | lastLine: 7, 31 | message: `Dict entry 0 has incompatible type "str": "int"; expected "str": "str"`, 32 | }, 33 | { 34 | path: "file1.py", 35 | firstLine: 11, 36 | lastLine: 11, 37 | message: `Argument 1 to "main" has incompatible type "List[str]"; expected "str"`, 38 | }, 39 | ], 40 | }, 41 | }; 42 | } 43 | 44 | // Linting with auto-fixing 45 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 46 | 47 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 48 | -------------------------------------------------------------------------------- /.github/workflows/test-action.yml: -------------------------------------------------------------------------------- 1 | name: Test Action 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request_target: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | checks: write 13 | contents: read 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | name: Run action 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | 28 | steps: 29 | - name: Check out repository (push) 30 | if: ${{ github.event_name == 'push' }} 31 | uses: actions/checkout@v4 32 | 33 | - name: Check out repository (pull_request_target) 34 | if: ${{ github.event_name == 'pull_request_target' }} 35 | uses: actions/checkout@v4 36 | with: 37 | ref: ${{ github.event.pull_request.head.sha }} 38 | 39 | - name: Set up Node.js 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version-file: ".nvmrc" 43 | cache: "yarn" 44 | 45 | - name: Install dependencies 46 | run: yarn install 47 | 48 | - name: Build action 49 | run: yarn build 50 | 51 | - name: Run linters 52 | uses: ./ 53 | with: 54 | continue_on_error: false 55 | eslint: true 56 | prettier: true 57 | prettier_extensions: "css,html,js,json,jsx,less,md,scss,ts,tsx,vue,yaml,yml" 58 | neutral_check_on_warning: true 59 | -------------------------------------------------------------------------------- /test/linters/params/flake8.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | const { sep } = require("path"); 3 | 4 | const Flake8 = require("../../../src/linters/flake8"); 5 | 6 | const testName = "flake8"; 7 | const linter = Flake8; 8 | const args = ""; 9 | const commandPrefix = ""; 10 | const extensions = ["py"]; 11 | 12 | // Linting without auto-fixing 13 | function getLintParams(dir) { 14 | const stdoutFile1 = `.${sep}file1.py:5:9: E211 whitespace before '('${EOL}.${sep}file1.py:26:1: E305 expected 2 blank lines after class or function definition, found 1`; 15 | const stdoutFile2 = `.${sep}file2.py:2:3: E111 indentation is not a multiple of four`; 16 | return { 17 | // Expected output of the linting function 18 | cmdOutput: { 19 | status: 1, 20 | stdoutParts: [stdoutFile1, stdoutFile2], 21 | stdout: `${stdoutFile1}${EOL}${stdoutFile2}`, 22 | }, 23 | // Expected output of the parsing function 24 | lintResult: { 25 | isSuccess: false, 26 | warning: [], 27 | error: [ 28 | { 29 | path: "file1.py", 30 | firstLine: 5, 31 | lastLine: 5, 32 | message: "Whitespace before '(' (E211)", 33 | }, 34 | { 35 | path: "file1.py", 36 | firstLine: 26, 37 | lastLine: 26, 38 | message: "Expected 2 blank lines after class or function definition, found 1 (E305)", 39 | }, 40 | { 41 | path: "file2.py", 42 | firstLine: 2, 43 | lastLine: 2, 44 | message: "Indentation is not a multiple of four (E111)", 45 | }, 46 | ], 47 | }, 48 | }; 49 | } 50 | 51 | // Linting with auto-fixing 52 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 53 | 54 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 55 | -------------------------------------------------------------------------------- /test/linters/params/tsc.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | 3 | const TSC = require("../../../src/linters/tsc"); 4 | 5 | const testName = "tsc"; 6 | const linter = TSC; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["js"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const stdoutFile1 = `file1.ts(1,5): error TS7034: Variable 'str' implicitly has type 'any' in some locations where its type cannot be determined.${EOL}file1.ts(4,25): error TS7005: Variable 'str' implicitly has an 'any' type.`; 14 | const stdoutFile2 = `file2.ts(3,1): error TS2322: Type 'string' is not assignable to type 'number'.`; 15 | return { 16 | // Expected output of the linting function 17 | cmdOutput: { 18 | status: 2, 19 | stdoutParts: [stdoutFile1, stdoutFile2], 20 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 21 | }, 22 | // Expected output of the parsing function 23 | lintResult: { 24 | isSuccess: false, 25 | warning: [], // TSC only emits errors 26 | error: [ 27 | { 28 | path: "file1.ts", 29 | firstLine: 1, 30 | lastLine: 1, 31 | message: 32 | "TS7034: Variable 'str' implicitly has type 'any' in some locations where its type cannot be determined", 33 | }, 34 | { 35 | path: "file1.ts", 36 | firstLine: 4, 37 | lastLine: 4, 38 | message: "TS7005: Variable 'str' implicitly has an 'any' type", 39 | }, 40 | { 41 | path: "file2.ts", 42 | firstLine: 3, 43 | lastLine: 3, 44 | message: "TS2322: Type 'string' is not assignable to type 'number'", 45 | }, 46 | ], 47 | }, 48 | }; 49 | } 50 | 51 | // TSC does not support auto-fixing 52 | const getFixParams = getLintParams; 53 | 54 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 55 | -------------------------------------------------------------------------------- /src/linters/index.js: -------------------------------------------------------------------------------- 1 | const Autopep8 = require("./autopep8"); 2 | const Black = require("./black"); 3 | const ClangFormat = require("./clang-format"); 4 | const Clippy = require("./clippy"); 5 | const DotnetFormat = require("./dotnet-format"); 6 | const Erblint = require("./erblint"); 7 | const ESLint = require("./eslint"); 8 | const Flake8 = require("./flake8"); 9 | const Gofmt = require("./gofmt"); 10 | const Golint = require("./golint"); 11 | const Mypy = require("./mypy"); 12 | const Oitnb = require("./oitnb"); 13 | const PHPCodeSniffer = require("./php-codesniffer"); 14 | const Prettier = require("./prettier"); 15 | const Pylint = require("./pylint"); 16 | const RuboCop = require("./rubocop"); 17 | const RustFmt = require("./rustfmt"); 18 | const Stylelint = require("./stylelint"); 19 | const SwiftFormatLockwood = require("./swift-format-lockwood"); 20 | const SwiftFormatOfficial = require("./swift-format-official"); 21 | const SwiftLint = require("./swiftlint"); 22 | const TSC = require("./tsc"); 23 | const XO = require("./xo"); 24 | 25 | const linters = { 26 | // Linters 27 | clippy: Clippy, 28 | erblint: Erblint, 29 | eslint: ESLint, 30 | flake8: Flake8, 31 | golint: Golint, 32 | mypy: Mypy, 33 | php_codesniffer: PHPCodeSniffer, 34 | pylint: Pylint, 35 | rubocop: RuboCop, 36 | stylelint: Stylelint, 37 | swiftlint: SwiftLint, 38 | xo: XO, 39 | tsc: TSC, 40 | 41 | // Formatters (should be run after linters) 42 | autopep8: Autopep8, 43 | black: Black, 44 | clang_format: ClangFormat, 45 | dotnet_format: DotnetFormat, 46 | gofmt: Gofmt, 47 | oitnb: Oitnb, 48 | rustfmt: RustFmt, 49 | prettier: Prettier, 50 | swift_format_lockwood: SwiftFormatLockwood, 51 | swift_format_official: SwiftFormatOfficial, 52 | 53 | // Alias of `swift_format_lockwood` (for backward compatibility) 54 | // TODO: Remove alias in v2 55 | swiftformat: SwiftFormatLockwood, 56 | }; 57 | 58 | module.exports = linters; 59 | -------------------------------------------------------------------------------- /test/linters/params/prettier.js: -------------------------------------------------------------------------------- 1 | const Prettier = require("../../../src/linters/prettier"); 2 | 3 | const testName = "prettier"; 4 | const linter = Prettier; 5 | const args = ""; 6 | const commandPrefix = ""; 7 | const extensions = [ 8 | "css", 9 | "html", 10 | "js", 11 | "json", 12 | "jsx", 13 | "md", 14 | "sass", 15 | "scss", 16 | "ts", 17 | "tsx", 18 | "vue", 19 | "yaml", 20 | "yml", 21 | ]; 22 | 23 | // Linting without auto-fixing 24 | function getLintParams(dir) { 25 | const stdoutFile1 = `file1.js`; 26 | const stdoutFile2 = `file2.css`; 27 | return { 28 | // Expected output of the linting function 29 | cmdOutput: { 30 | status: 1, 31 | stdoutParts: [stdoutFile1, stdoutFile2], 32 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 33 | }, 34 | // Expected output of the parsing function 35 | lintResult: { 36 | isSuccess: false, 37 | warning: [], 38 | error: [ 39 | { 40 | path: "file1.js", 41 | firstLine: 1, 42 | lastLine: 1, 43 | message: 44 | "There are issues with this file's formatting, please run Prettier to fix the errors", 45 | }, 46 | { 47 | path: "file2.css", 48 | firstLine: 1, 49 | lastLine: 1, 50 | message: 51 | "There are issues with this file's formatting, please run Prettier to fix the errors", 52 | }, 53 | ], 54 | }, 55 | }; 56 | } 57 | 58 | // Linting with auto-fixing 59 | function getFixParams(dir) { 60 | const stdoutFile1 = `file1.js`; 61 | const stdoutFile2 = `file2.css`; 62 | return { 63 | // Expected output of the linting function 64 | cmdOutput: { 65 | status: 0, 66 | stdoutParts: [stdoutFile1, stdoutFile2], 67 | }, 68 | // Expected output of the parsing function 69 | lintResult: { 70 | isSuccess: true, 71 | warning: [], 72 | error: [], 73 | }, 74 | }; 75 | } 76 | 77 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 78 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | const { mkdtempSync, realpathSync } = require("fs"); 2 | const { join } = require("path"); 3 | 4 | const DATE_REGEX = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+ ?[+-]\d{2}:?\d{2}/g; 5 | const TEST_DATE = "2019-01-01 00:00:00.000000 +0000"; 6 | 7 | /** 8 | * Creates a temporary directory. 9 | * @returns {string} - File path 10 | */ 11 | function createTmpDir() { 12 | return mkdtempSync(join(__dirname, "tmp-")); 13 | } 14 | 15 | /** 16 | * Some tools require paths to contain single forward slashes on macOS/Linux and double backslashes 17 | * on Windows. This is an extended `path.join` function that corrects these path separators 18 | * @param {...string} paths - Paths to join 19 | * @returns {string} - File path 20 | */ 21 | function joinDoubleBackslash(...paths) { 22 | let filePath = join(...paths); 23 | if (process.platform === "win32") { 24 | filePath = filePath.replace(/\\/g, "\\\\"); 25 | } 26 | return filePath; 27 | } 28 | 29 | /** 30 | * Some tools output real paths for files. This function corrects these paths to use the provided 31 | * path. 32 | * @param {string} str - String in which paths should be replaced 33 | * @param {string} path - Which path should be replaced 34 | * @returns {string} - Normalized paths 35 | */ 36 | function normalizePaths(str, path) { 37 | const pathToSearch = realpathSync(path); 38 | const pathToSearchEscaped = pathToSearch.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 39 | return str.replace(new RegExp(pathToSearchEscaped, "g"), path); 40 | } 41 | 42 | /** 43 | * Find dates in the provided string and replace them with {@link TEST_DATE} 44 | * @param {string} str - String in which dates should be replaced 45 | * @returns {string} - Normalized date 46 | */ 47 | function normalizeDates(str) { 48 | return str.replace(DATE_REGEX, TEST_DATE); 49 | } 50 | 51 | module.exports = { TEST_DATE, joinDoubleBackslash, normalizeDates, normalizePaths, createTmpDir }; 52 | -------------------------------------------------------------------------------- /test/linters/params/clang-format.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | 3 | const ClangFormat = require("../../../src/linters/clang-format"); 4 | 5 | const testName = "clang-format"; 6 | const linter = ClangFormat; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["c", "mm"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const stderrFile1 = [ 14 | "file1.c:1:1: error: code should be clang-formatted [-Wclang-format-violations]", 15 | " #include ", 16 | "^^^^^^^^", 17 | ].join(EOL); 18 | const stderrFile2 = [ 19 | "file2.mm:1:26: error: code should be clang-formatted [-Wclang-format-violations]", 20 | "@interface Foo : NSObject @end", 21 | " ^", 22 | ].join(EOL); 23 | return { 24 | // Expected output of the linting function 25 | cmdOutput: { 26 | status: 1, 27 | stderrParts: [stderrFile1, stderrFile2], 28 | stderr: `${stderrFile1}\n${stderrFile2}`, 29 | }, 30 | // Expected output of the parsing function 31 | lintResult: { 32 | isSuccess: false, 33 | warning: [], 34 | error: [ 35 | { 36 | path: "file1.c", 37 | firstLine: 1, 38 | lastLine: 1, 39 | message: "code should be clang-formatted [-Wclang-format-violations]", 40 | }, 41 | { 42 | path: "file2.mm", 43 | firstLine: 1, 44 | lastLine: 1, 45 | message: "code should be clang-formatted [-Wclang-format-violations]", 46 | }, 47 | ], 48 | }, 49 | }; 50 | } 51 | 52 | // Linting with auto-fixing 53 | function getFixParams(dir) { 54 | return { 55 | // Expected output of the linting function 56 | cmdOutput: { 57 | status: 0, 58 | stderrParts: [], 59 | }, 60 | // Expected output of the parsing function 61 | lintResult: { 62 | isSuccess: true, 63 | warning: [], 64 | error: [], 65 | }, 66 | }; 67 | } 68 | 69 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Suggestions and contributions are always welcome! Please discuss larger changes via issue before submitting a pull request. 4 | 5 | ## Adding a new linter 6 | 7 | If you want to add support for an additional linter, please open an issue to discuss its inclusion in the project. Afterwards, you can follow these steps to add support for your linter: 8 | 9 | - Clone the repository and install its dependencies with `yarn install`. 10 | - Create a new class for the linter, e.g. `src/linters/my-linter.js`. Have a look at the other files in that directory to see what functions the class needs to implement. 11 | - Import your class in the [`src/linters/index.js`](./src/linters/index.js) file. 12 | - Provide a sample project for the linter under `test/linters/projects/my-linter/`. It should be simple and contain a few linting errors which your tests will detect. 13 | - Provide the expected linting output for your sample project in a `test/linters/params/my-linter.js` file. Import this file in [`test/linters/linters.test.js`](./test/linters/linters.test.js). You can run the tests with `yarn test`. 14 | - Update the [`action.yml`](./action.yml) file with the options provided by the new linter. 15 | - Mention your linter in the [`README.md`](./README.md) file. 16 | - Update the [test workflow file](./.github/workflows/test.yml). 17 | 18 | ## Release process 19 | 20 | To release a new version using semantic versioning follow these steps: 21 | 22 | 1. Bump the version in `package.json`. 23 | 2. Create a commit with a message like "v1.1.1". 24 | 3. Tag that commit with the same message. 25 | 4. Create a release from that tag on GitHub (this will publish it on the Action Marketplace, too) with a changelog of the user-facing changes. 26 | 27 | ### Changelog format: 28 | 29 | - Message (PR number, commit hash) 30 | - Message (PR number, commit hash) 31 | - … 32 | 33 | https://github.com/wearerequired/lint-action/compare/... 34 | -------------------------------------------------------------------------------- /src/linters/xo.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { getNpmBinCommand } = require("../utils/npm/get-npm-bin-command"); 4 | const ESLint = require("./eslint"); 5 | 6 | /** 7 | * https://github.com/xojs/xo 8 | * XO is a wrapper for ESLint, so it can use the same logic for parsing lint results 9 | */ 10 | class XO extends ESLint { 11 | static get name() { 12 | return "XO"; 13 | } 14 | 15 | /** 16 | * Verifies that all required programs are installed. Throws an error if programs are missing 17 | * @param {string} dir - Directory to run the linting program in 18 | * @param {string} prefix - Prefix to the lint command 19 | */ 20 | static async verifySetup(dir, prefix = "") { 21 | // Verify that NPM is installed (required to execute XO) 22 | if (!(await commandExists("npm"))) { 23 | throw new Error("NPM is not installed"); 24 | } 25 | 26 | // Verify that XO is installed 27 | const commandPrefix = prefix || getNpmBinCommand(dir); 28 | try { 29 | run(`${commandPrefix} xo --version`, { dir }); 30 | } catch (err) { 31 | throw new Error(`${this.name} is not installed`); 32 | } 33 | } 34 | 35 | /** 36 | * Runs the linting program and returns the command output 37 | * @param {string} dir - Directory to run the linter in 38 | * @param {string[]} extensions - File extensions which should be linted 39 | * @param {string} args - Additional arguments to pass to the linter 40 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 41 | * @param {string} prefix - Prefix to the lint command 42 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 43 | */ 44 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 45 | const extensionArgs = extensions.map((ext) => `--extension ${ext}`).join(" "); 46 | const fixArg = fix ? "--fix" : ""; 47 | const commandPrefix = prefix || getNpmBinCommand(dir); 48 | return run(`${commandPrefix} xo ${extensionArgs} ${fixArg} --reporter json ${args} "."`, { 49 | dir, 50 | ignoreErrors: true, 51 | }); 52 | } 53 | } 54 | 55 | module.exports = XO; 56 | -------------------------------------------------------------------------------- /test/linters/params/autopep8.js: -------------------------------------------------------------------------------- 1 | const Autopep8 = require("../../../src/linters/autopep8"); 2 | 3 | const testName = "autopep8"; 4 | const linter = Autopep8; 5 | const commandPrefix = ""; 6 | const args = ""; 7 | const extensions = ["py"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdoutFile1 = `--- file1.py\n+++ file1.py\n@@ -2,7 +2,7 @@\n var_2 = "world"\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n print("hello " + var_2)\n \n \n@@ -23,4 +23,5 @@\n \n # Blank lines error\n \n+\n main()`; 12 | const stdoutFile2 = `--- file2.py\n+++ file2.py\n@@ -1,2 +1,2 @@\n def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`; 13 | return { 14 | // Expected output of the linting function 15 | cmdOutput: { 16 | status: 2, 17 | stdoutParts: [stdoutFile1, stdoutFile2], 18 | stdout: `${stdoutFile1}\n \n${stdoutFile2}`, 19 | }, 20 | // Expected output of the parsing function 21 | lintResult: { 22 | isSuccess: false, 23 | warning: [], 24 | error: [ 25 | { 26 | path: "file1.py", 27 | firstLine: 2, 28 | lastLine: 9, 29 | message: ` var_2 = "world"\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n print("hello " + var_2)\n \n `, 30 | }, 31 | { 32 | path: "file1.py", 33 | firstLine: 23, 34 | lastLine: 27, 35 | message: ` \n # Blank lines error\n \n+\n main()`, 36 | }, 37 | { 38 | path: "file2.py", 39 | firstLine: 1, 40 | lastLine: 3, 41 | message: ` def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`, 42 | }, 43 | ], 44 | }, 45 | }; 46 | } 47 | 48 | // Linting with auto-fixing 49 | function getFixParams(dir) { 50 | return { 51 | // Expected output of the linting function 52 | cmdOutput: { 53 | status: 0, 54 | stdout: "", 55 | }, 56 | // Expected output of the parsing function 57 | lintResult: { 58 | isSuccess: true, 59 | warning: [], 60 | error: [], 61 | }, 62 | }; 63 | } 64 | 65 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 66 | -------------------------------------------------------------------------------- /src/utils/action.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | 3 | const core = require("@actions/core"); 4 | 5 | const RUN_OPTIONS_DEFAULTS = { dir: null, ignoreErrors: false, prefix: "" }; 6 | 7 | /** 8 | * Returns the value for an environment variable. If the variable is required but doesn't have a 9 | * value, an error is thrown 10 | * @param {string} name - Name of the environment variable 11 | * @param {boolean} required - Whether an error should be thrown if the variable doesn't have a 12 | * value 13 | * @returns {string | null} - Value of the environment variable 14 | */ 15 | function getEnv(name, required = false) { 16 | const nameUppercase = name.toUpperCase(); 17 | const value = process.env[nameUppercase]; 18 | if (value == null) { 19 | // Value is either not set (`undefined`) or set to `null` 20 | if (required) { 21 | throw new Error(`Environment variable "${nameUppercase}" is not defined`); 22 | } 23 | return null; 24 | } 25 | return value; 26 | } 27 | 28 | /** 29 | * Executes the provided shell command 30 | * @param {string} cmd - Shell command to execute 31 | * @param {{dir: string, ignoreErrors: boolean}} [options] - {@see RUN_OPTIONS_DEFAULTS} 32 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the shell command 33 | */ 34 | function run(cmd, options) { 35 | const optionsWithDefaults = { 36 | ...RUN_OPTIONS_DEFAULTS, 37 | ...options, 38 | }; 39 | 40 | core.debug(cmd); 41 | 42 | try { 43 | const stdout = execSync(cmd, { 44 | encoding: "utf8", 45 | cwd: optionsWithDefaults.dir, 46 | maxBuffer: 20 * 1024 * 1024, 47 | }); 48 | const output = { 49 | status: 0, 50 | stdout: stdout.trim(), 51 | stderr: "", 52 | }; 53 | 54 | core.debug(`Stdout: ${output.stdout}`); 55 | 56 | return output; 57 | } catch (err) { 58 | if (optionsWithDefaults.ignoreErrors) { 59 | const output = { 60 | status: err.status, 61 | stdout: err.stdout.trim(), 62 | stderr: err.stderr.trim(), 63 | }; 64 | 65 | core.debug(`Exit code: ${output.status}`); 66 | core.debug(`Stdout: ${output.stdout}`); 67 | core.debug(`Stderr: ${output.stderr}`); 68 | 69 | return output; 70 | } 71 | throw err; 72 | } 73 | } 74 | 75 | module.exports = { 76 | getEnv, 77 | run, 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lint-action", 3 | "version": "2.3.0", 4 | "description": "GitHub Action for detecting and fixing linting errors", 5 | "repository": "github:wearerequired/lint-action", 6 | "license": "MIT", 7 | "private": true, 8 | "main": "./dist/index.js", 9 | "scripts": { 10 | "test": "jest", 11 | "lint": "eslint --max-warnings 0 \"**/*.js\"", 12 | "lint:fix": "yarn lint --fix", 13 | "format": "prettier --list-different \"**/*.{css,html,js,json,jsx,less,md,scss,ts,tsx,vue,yaml,yml}\"", 14 | "format:fix": "yarn format --write", 15 | "build": "ncc build ./src/index.js" 16 | }, 17 | "dependencies": { 18 | "@actions/core": "^1.10.0", 19 | "command-exists": "^1.2.9", 20 | "glob": "^8.1.0", 21 | "parse-diff": "^0.11.0", 22 | "shescape": "^2.1.0" 23 | }, 24 | "peerDependencies": {}, 25 | "devDependencies": { 26 | "@samuelmeuli/eslint-config": "^6.0.0", 27 | "@samuelmeuli/prettier-config": "^2.0.1", 28 | "@vercel/ncc": "^0.38.1", 29 | "eslint": "8.32.0", 30 | "eslint-config-airbnb-base": "15.0.0", 31 | "eslint-config-prettier": "^8.6.0", 32 | "eslint-plugin-import": "^2.26.0", 33 | "eslint-plugin-jsdoc": "^48.0.0", 34 | "fs-extra": "^11.1.0", 35 | "jest": "^29.3.1", 36 | "prettier": "^2.8.3" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "extends": [ 41 | "@samuelmeuli/eslint-config", 42 | "plugin:jsdoc/recommended" 43 | ], 44 | "env": { 45 | "node": true, 46 | "jest": true 47 | }, 48 | "settings": { 49 | "jsdoc": { 50 | "mode": "typescript" 51 | } 52 | }, 53 | "rules": { 54 | "no-await-in-loop": "off", 55 | "no-unused-vars": [ 56 | "error", 57 | { 58 | "args": "none", 59 | "varsIgnorePattern": "^_" 60 | } 61 | ], 62 | "jsdoc/check-indentation": "error", 63 | "jsdoc/check-syntax": "error", 64 | "jsdoc/tag-lines": "error", 65 | "jsdoc/require-description": "error", 66 | "jsdoc/require-hyphen-before-param-description": "error", 67 | "jsdoc/require-jsdoc": "off" 68 | } 69 | }, 70 | "eslintIgnore": [ 71 | "node_modules/", 72 | "test/linters/projects/", 73 | "test/tmp/", 74 | "dist/" 75 | ], 76 | "jest": { 77 | "setupFiles": [ 78 | "./test/mock-actions-core.js" 79 | ] 80 | }, 81 | "prettier": "@samuelmeuli/prettier-config" 82 | } 83 | -------------------------------------------------------------------------------- /test/linters/params/erblint.js: -------------------------------------------------------------------------------- 1 | const Erblint = require("../../../src/linters/erblint"); 2 | 3 | const testName = "erblint"; 4 | const linter = Erblint; 5 | const args = ""; 6 | const commandPrefix = "bundle exec"; 7 | const extensions = ["erb"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdout1 = 12 | '{"path":"file1.erb","offenses":[{"linter":"SpaceAroundErbTag","message":"Use 1 space before `%>` instead of 2 space.","location":{"start_line":3,"start_column":6,"last_line":3,"last_column":8,"length":2}}]}'; 13 | const stdout2 = 14 | '{"path":"file2.erb","offenses":[{"linter":"SpaceInHtmlTag","message":"No space detected where there should be a single space.","location":{"start_line":2,"start_column":3,"last_line":2,"last_column":3,"length":0}},{"linter":"SelfClosingTag","message":"Tag `br` is a void element, it must end with `>` and not `/>`.","location":{"start_line":2,"start_column":3,"last_line":2,"last_column":4,"length":1}}]}'; 15 | return { 16 | // Expected output of the linting function 17 | cmdOutput: { 18 | status: 1, 19 | stdoutParts: [], 20 | stdout: `{"metadata":{"erb_lint_version":"0.1.1","ruby_engine":"ruby","ruby_version":"2.6.8","ruby_patchlevel":"205","ruby_platform":"x86_64-darwin20"},"files":[${stdout1}, ${stdout2}],"summary":{"offenses":3,"inspected_files":2,"corrected":0}}`, 21 | }, 22 | // Expected output of the parsing function 23 | lintResult: { 24 | isSuccess: false, 25 | error: [ 26 | { 27 | path: "file1.erb", 28 | firstLine: 3, 29 | lastLine: 3, 30 | message: "Use 1 space before `%>` instead of 2 space (SpaceAroundErbTag)", 31 | }, 32 | { 33 | path: "file2.erb", 34 | firstLine: 2, 35 | lastLine: 2, 36 | message: "No space detected where there should be a single space (SpaceInHtmlTag)", 37 | }, 38 | { 39 | path: "file2.erb", 40 | firstLine: 2, 41 | lastLine: 2, 42 | message: "Tag `br` is a void element, it must end with `>` and not `/>` (SelfClosingTag)", 43 | }, 44 | ], 45 | warning: [], 46 | }, 47 | }; 48 | } 49 | 50 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 51 | 52 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 53 | -------------------------------------------------------------------------------- /test/linters/params/dotnet-format.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | const { join } = require("path"); 3 | 4 | const DotnetFormat = require("../../../src/linters/dotnet-format"); 5 | 6 | const testName = "dotnet-format"; 7 | const linter = DotnetFormat; 8 | const args = ""; 9 | const commandPrefix = ""; 10 | const extensions = ["cs"]; 11 | 12 | // Linting without auto-fixing 13 | function getLintParams(dir) { 14 | const stderrPart1 = `${join( 15 | dir, 16 | "file2.cs", 17 | )}(20,31): error WHITESPACE: Fix whitespace formatting. Delete 1 characters. [${join( 18 | dir, 19 | "dotnet-format.csproj", 20 | )}`; 21 | const stderrPart2 = `${join(dir, "file2.cs")}(1,1): error IMPORTS: Fix imports ordering. [${join( 22 | dir, 23 | "dotnet-format.csproj", 24 | )}`; 25 | const stderrPart3 = `${join( 26 | dir, 27 | "file1.cs", 28 | )}(1,1): warning IDE0073: A source file is missing a required header. [${join( 29 | dir, 30 | "dotnet-format.csproj", 31 | )}`; 32 | return { 33 | // Expected output of the linting function 34 | cmdOutput: { 35 | status: 2, 36 | stderrParts: [stderrPart1, stderrPart2, stderrPart3], 37 | stderr: `${stderrPart1}${EOL}${stderrPart2}${EOL}${stderrPart3}`, 38 | }, 39 | // Expected output of the parsing function 40 | lintResult: { 41 | isSuccess: false, 42 | warning: [ 43 | { 44 | path: "file1.cs", 45 | firstLine: 1, 46 | lastLine: 1, 47 | message: `IDE0073: A source file is missing a required header.`, 48 | }, 49 | ], 50 | error: [ 51 | { 52 | path: "file2.cs", 53 | firstLine: 20, 54 | lastLine: 20, 55 | message: `WHITESPACE: Fix whitespace formatting. Delete 1 characters.`, 56 | }, 57 | { 58 | path: "file2.cs", 59 | firstLine: 1, 60 | lastLine: 1, 61 | message: `IMPORTS: Fix imports ordering.`, 62 | }, 63 | ], 64 | }, 65 | }; 66 | } 67 | 68 | // Linting with auto-fixing 69 | function getFixParams(dir) { 70 | return { 71 | // Expected output of the linting function 72 | cmdOutput: { 73 | status: 0, 74 | stderr: ``, 75 | }, 76 | // Expected output of the parsing function 77 | lintResult: { 78 | isSuccess: true, 79 | warning: [], 80 | error: [], 81 | }, 82 | }; 83 | } 84 | 85 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 86 | -------------------------------------------------------------------------------- /test/linters/projects/erblint/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionview (7.0.4.1) 5 | activesupport (= 7.0.4.1) 6 | builder (~> 3.1) 7 | erubi (~> 1.4) 8 | rails-dom-testing (~> 2.0) 9 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 10 | activesupport (7.0.4.1) 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | i18n (>= 1.6, < 2) 13 | minitest (>= 5.1) 14 | tzinfo (~> 2.0) 15 | ast (2.4.2) 16 | better_html (1.0.16) 17 | actionview (>= 4.0) 18 | activesupport (>= 4.0) 19 | ast (~> 2.0) 20 | erubi (~> 1.4) 21 | html_tokenizer (~> 0.0.6) 22 | parser (>= 2.4) 23 | smart_properties 24 | builder (3.2.4) 25 | concurrent-ruby (1.1.10) 26 | crass (1.0.6) 27 | erb_lint (0.1.3) 28 | activesupport 29 | better_html (~> 1.0.7) 30 | html_tokenizer 31 | parser (>= 2.7.1.4) 32 | rainbow 33 | rubocop 34 | smart_properties 35 | erubi (1.12.0) 36 | html_tokenizer (0.0.7) 37 | i18n (1.12.0) 38 | concurrent-ruby (~> 1.0) 39 | loofah (2.19.1) 40 | crass (~> 1.0.2) 41 | nokogiri (>= 1.5.9) 42 | mini_portile2 (2.8.1) 43 | minitest (5.17.0) 44 | nokogiri (1.13.10) 45 | mini_portile2 (~> 2.8.0) 46 | racc (~> 1.4) 47 | parallel (1.22.1) 48 | parser (3.1.3.0) 49 | ast (~> 2.4.1) 50 | racc (1.6.2) 51 | rails-dom-testing (2.0.3) 52 | activesupport (>= 4.2.0) 53 | nokogiri (>= 1.6) 54 | rails-html-sanitizer (1.4.4) 55 | loofah (~> 2.19, >= 2.19.1) 56 | rainbow (3.1.1) 57 | regexp_parser (2.6.1) 58 | rexml (3.2.5) 59 | rubocop (1.12.1) 60 | parallel (~> 1.10) 61 | parser (>= 3.0.0.0) 62 | rainbow (>= 2.2.2, < 4.0) 63 | regexp_parser (>= 1.8, < 3.0) 64 | rexml 65 | rubocop-ast (>= 1.2.0, < 2.0) 66 | ruby-progressbar (~> 1.7) 67 | unicode-display_width (>= 1.4.0, < 3.0) 68 | rubocop-ast (1.24.1) 69 | parser (>= 3.1.1.0) 70 | ruby-progressbar (1.11.0) 71 | smart_properties (1.17.0) 72 | tzinfo (2.0.5) 73 | concurrent-ruby (~> 1.0) 74 | unicode-display_width (2.3.0) 75 | 76 | PLATFORMS 77 | ruby 78 | 79 | DEPENDENCIES 80 | erb_lint (~> 0.1.1) 81 | rubocop (~> 1.12.0) 82 | 83 | BUNDLED WITH 84 | 2.1.4 85 | -------------------------------------------------------------------------------- /test/linters/params/swift-format-official.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const SwiftFormatOfficial = require("../../../src/linters/swift-format-official"); 4 | 5 | const testName = "swift-format-official"; 6 | const linter = SwiftFormatOfficial; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["swift"]; 10 | 11 | function getLintParams(dir) { 12 | const warning1 = `${join(dir, "file2.swift")}:2:22: warning: [DoNotUseSemicolons]: remove ';'`; 13 | const warning2 = `${join(dir, "file1.swift")}:3:35: warning: [RemoveLine]: remove line break`; 14 | const warning3 = `${join( 15 | dir, 16 | "file1.swift", 17 | )}:7:1: warning: [Indentation] replace leading whitespace with 2 spaces`; 18 | const warning4 = `${join(dir, "file1.swift")}:7:23: warning: [Spacing]: add 1 space`; 19 | // Files on macOS are not sorted. 20 | const stderr = 21 | process.platform === "darwin" 22 | ? `${warning2}\n${warning3}\n${warning4}\n${warning1}` 23 | : `${warning1}\n${warning2}\n${warning3}\n${warning4}`; 24 | return { 25 | // Expected output of the linting function. 26 | cmdOutput: { 27 | status: 0, 28 | stderrParts: [warning1, warning2, warning3, warning4], 29 | stderr, 30 | }, 31 | // Expected output of the parsing function. 32 | lintResult: { 33 | isSuccess: false, 34 | error: [ 35 | { 36 | path: "file2.swift", 37 | firstLine: 2, 38 | lastLine: 2, 39 | message: "[DoNotUseSemicolons]: remove ';'", 40 | }, 41 | { 42 | path: "file1.swift", 43 | firstLine: 3, 44 | lastLine: 3, 45 | message: "[RemoveLine]: remove line break", 46 | }, 47 | { 48 | path: "file1.swift", 49 | firstLine: 7, 50 | lastLine: 7, 51 | message: "[Indentation] replace leading whitespace with 2 spaces", 52 | }, 53 | { 54 | path: "file1.swift", 55 | firstLine: 7, 56 | lastLine: 7, 57 | message: "[Spacing]: add 1 space", 58 | }, 59 | ], 60 | warning: [], 61 | }, 62 | }; 63 | } 64 | 65 | function getFixParams(dir) { 66 | return { 67 | // Expected output of the linting function. 68 | cmdOutput: { 69 | status: 0, 70 | stderr: "", 71 | }, 72 | // Expected output of the parsing function. 73 | lintResult: { 74 | isSuccess: true, 75 | warning: [], 76 | error: [], 77 | }, 78 | }; 79 | } 80 | 81 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 82 | -------------------------------------------------------------------------------- /test/linters/params/black.js: -------------------------------------------------------------------------------- 1 | const Black = require("../../../src/linters/black"); 2 | const { TEST_DATE } = require("../../test-utils"); 3 | 4 | const testName = "black"; 5 | const linter = Black; 6 | const args = ""; 7 | const commandPrefix = ""; 8 | const extensions = ["py"]; 9 | 10 | // Linting without auto-fixing 11 | function getLintParams(dir) { 12 | const stdoutFile1 = `--- file1.py ${TEST_DATE}\n+++ file1.py ${TEST_DATE}\n@@ -1,10 +1,10 @@\n var_1 = "hello"\n var_2 = "world"\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n print("hello " + var_2)\n \n \n def add(num_1, num_2):\n return num_1 + num_2\n@@ -19,8 +19,9 @@\n \n \n def divide(num_1, num_2):\n return num_1 / num_2\n \n+\n # Blank lines error\n \n main()`; 13 | const stdoutFile2 = `--- file2.py ${TEST_DATE}\n+++ file2.py ${TEST_DATE}\n@@ -1,2 +1,2 @@\n def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`; 14 | return { 15 | // Expected output of the linting function 16 | cmdOutput: { 17 | status: 1, 18 | stdoutParts: [stdoutFile1, stdoutFile2], 19 | stdout: `${stdoutFile1}\n \n${stdoutFile2}`, 20 | }, 21 | // Expected output of the parsing function 22 | lintResult: { 23 | isSuccess: false, 24 | warning: [], 25 | error: [ 26 | { 27 | path: "file1.py", 28 | firstLine: 1, 29 | lastLine: 11, 30 | message: ` var_1 = "hello"\n var_2 = "world"\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n print("hello " + var_2)\n \n \n def add(num_1, num_2):\n return num_1 + num_2`, 31 | }, 32 | { 33 | path: "file1.py", 34 | firstLine: 19, 35 | lastLine: 27, 36 | message: ` \n \n def divide(num_1, num_2):\n return num_1 / num_2\n \n+\n # Blank lines error\n \n main()`, 37 | }, 38 | { 39 | path: "file2.py", 40 | firstLine: 1, 41 | lastLine: 3, 42 | message: ` def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`, 43 | }, 44 | ], 45 | }, 46 | }; 47 | } 48 | 49 | // Linting with auto-fixing 50 | function getFixParams(dir) { 51 | return { 52 | // Expected output of the linting function 53 | cmdOutput: { 54 | status: 0, 55 | stdout: "", 56 | }, 57 | // Expected output of the parsing function 58 | lintResult: { 59 | isSuccess: true, 60 | warning: [], 61 | error: [], 62 | }, 63 | }; 64 | } 65 | 66 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 67 | -------------------------------------------------------------------------------- /test/linters/params/swift-format-lockwood.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const SwiftFormatLockwood = require("../../../src/linters/swift-format-lockwood"); 4 | 5 | const testName = "swift-format-lockwood"; 6 | const linter = SwiftFormatLockwood; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["swift"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const warning1 = `${join( 14 | dir, 15 | "file1.swift", 16 | )}:5:1: warning: (consecutiveBlankLines) Replace consecutive blank lines with a single blank line.`; 17 | const warning2 = `${join( 18 | dir, 19 | "file1.swift", 20 | )}:7:1: warning: (indent) Indent code in accordance with the scope level.`; 21 | const warning3 = `${join(dir, "file2.swift")}:2:1: warning: (semicolons) Remove semicolons.`; 22 | return { 23 | // Expected output of the linting function 24 | cmdOutput: { 25 | status: 1, 26 | stderrParts: [warning1, warning2, warning3], 27 | stderr: `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.`, 28 | }, 29 | // Expected output of the parsing function 30 | lintResult: { 31 | isSuccess: false, 32 | warning: [], 33 | error: [ 34 | { 35 | path: "file1.swift", 36 | firstLine: 5, 37 | lastLine: 5, 38 | message: 39 | "Replace consecutive blank lines with a single blank line (consecutiveBlankLines)", 40 | }, 41 | { 42 | path: "file1.swift", 43 | firstLine: 7, 44 | lastLine: 7, 45 | message: "Indent code in accordance with the scope level (indent)", 46 | }, 47 | { 48 | path: "file2.swift", 49 | firstLine: 2, 50 | lastLine: 2, 51 | message: "Remove semicolons (semicolons)", 52 | }, 53 | ], 54 | }, 55 | }; 56 | } 57 | 58 | // Linting with auto-fixing 59 | function getFixParams(dir) { 60 | return { 61 | // Expected output of the linting function 62 | cmdOutput: { 63 | status: 0, 64 | stderr: "", 65 | }, 66 | // Expected output of the parsing function 67 | lintResult: { 68 | isSuccess: true, 69 | warning: [], 70 | error: [], 71 | }, 72 | }; 73 | } 74 | 75 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 76 | -------------------------------------------------------------------------------- /test/linters/params/gofmt.js: -------------------------------------------------------------------------------- 1 | const Gofmt = require("../../../src/linters/gofmt"); 2 | 3 | const testName = "gofmt"; 4 | const linter = Gofmt; 5 | const args = ""; 6 | const commandPrefix = ""; 7 | const extensions = ["go"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdoutFile1 = `diff file1.go.orig file1.go\n--- file1.go.orig\n+++ file1.go\n@@ -4,7 +4,7 @@\n \n var str = "world"\n \n-func main () { // Whitespace error\n+func main() { // Whitespace error\n fmt.Println("hello " + str)\n }\n \n@@ -17,5 +17,5 @@\n }\n \n func multiply(num1 int, num2 int) int {\n- return num1 * num2 // Indentation error\n+ return num1 * num2 // Indentation error\n }`; 12 | const stdoutFile2 = `diff file2.go.orig file2.go\n--- file2.go.orig\n+++ file2.go\n@@ -1,5 +1,5 @@\n package main\n \n func divide(num1 int, num2 int) int {\n- return num1 / num2 // Whitespace error\n+ return num1 / num2 // Whitespace error\n }`; 13 | return { 14 | // Expected output of the linting function 15 | cmdOutput: { 16 | status: 0, // gofmt always uses exit code 0 17 | stdoutParts: [stdoutFile1, stdoutFile2], 18 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 19 | }, 20 | // Expected output of the parsing function 21 | lintResult: { 22 | isSuccess: false, 23 | warning: [], 24 | error: [ 25 | { 26 | path: "file1.go", 27 | firstLine: 4, 28 | lastLine: 11, 29 | message: ` \n var str = "world"\n \n-func main () { // Whitespace error\n+func main() { // Whitespace error\n \tfmt.Println("hello " + str)\n }\n `, 30 | }, 31 | { 32 | path: "file1.go", 33 | firstLine: 17, 34 | lastLine: 22, 35 | message: ` }\n \n func multiply(num1 int, num2 int) int {\n- return num1 * num2 // Indentation error\n+\treturn num1 * num2 // Indentation error\n }`, 36 | }, 37 | { 38 | path: "file2.go", 39 | firstLine: 1, 40 | lastLine: 6, 41 | message: ` package main\n \n func divide(num1 int, num2 int) int {\n-\treturn num1 / num2 // Whitespace error\n+\treturn num1 / num2 // Whitespace error\n }`, 42 | }, 43 | ], 44 | }, 45 | }; 46 | } 47 | 48 | // Linting with auto-fixing 49 | function getFixParams(dir) { 50 | return { 51 | // Expected output of the linting function 52 | cmdOutput: { 53 | status: 0, // gofmt always uses exit code 0 54 | stdout: "", 55 | }, 56 | // Expected output of the parsing function 57 | lintResult: { 58 | isSuccess: true, 59 | warning: [], 60 | error: [], 61 | }, 62 | }; 63 | } 64 | 65 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 66 | -------------------------------------------------------------------------------- /test/linters/params/oitnb.js: -------------------------------------------------------------------------------- 1 | const Oitnb = require("../../../src/linters/oitnb"); 2 | const { TEST_DATE } = require("../../test-utils"); 3 | 4 | const testName = "oitnb"; 5 | const linter = Oitnb; 6 | const args = ""; 7 | const commandPrefix = ""; 8 | const extensions = ["py"]; 9 | 10 | // Linting without auto-fixing 11 | function getLintParams(dir) { 12 | const stdoutFile1 = `--- file1.py ${TEST_DATE}\n+++ file1.py ${TEST_DATE}\n@@ -1,10 +1,10 @@\n var_1 = "hello"\n var_2 = "world"\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n print("hello " + var_2)\n \n \n def add(num_1, num_2):\n return num_1 + num_2\n@@ -19,8 +19,9 @@\n \n \n def divide(num_1, num_2):\n return num_1 / num_2\n \n+\n # Blank lines error\n \n main()`; 13 | const stdoutFile2 = `--- file2.py ${TEST_DATE}\n+++ file2.py ${TEST_DATE}\n@@ -1,2 +1,2 @@\n def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`; 14 | return { 15 | // Expected output of the linting function 16 | cmdOutput: { 17 | status: 1, 18 | stdoutParts: [stdoutFile1, stdoutFile2], 19 | stdout: `${stdoutFile1}\n \n${stdoutFile2}`, 20 | }, 21 | // Expected output of the parsing function 22 | lintResult: { 23 | isSuccess: false, 24 | warning: [], 25 | error: [ 26 | { 27 | path: "file1.py", 28 | firstLine: 1, 29 | lastLine: 11, 30 | message: ` -var_1 = "hello"\n+var_1 = 'hello'\n -var_2 = "world"\n+-var_2 = 'world'\n \n \n-def main (): # Whitespace error\n+def main(): # Whitespace error\n -print("hello " + var_2)\n+print('hello ' + var_2)\n \n \n def add(num_1, num_2):\n return num_1 + num_2`, 31 | }, 32 | { 33 | path: "file1.py", 34 | firstLine: 19, 35 | lastLine: 27, 36 | message: ` \n \n def divide(num_1, num_2):\n return num_1 / num_2\n \n+\n # Blank lines error\n \n main()\n `, 37 | }, 38 | { 39 | path: "file2.py", 40 | firstLine: 1, 41 | lastLine: 3, 42 | message: ` def add(num_1, num_2):\n- return num_1 + num_2 # Indentation error\n+ return num_1 + num_2 # Indentation error`, 43 | }, 44 | ], 45 | }, 46 | }; 47 | } 48 | 49 | // Linting with auto-fixing 50 | function getFixParams(dir) { 51 | return { 52 | // Expected output of the linting function 53 | cmdOutput: { 54 | status: 0, 55 | stdout: "", 56 | }, 57 | // Expected output of the parsing function 58 | lintResult: { 59 | isSuccess: true, 60 | warning: [], 61 | error: [], 62 | }, 63 | }; 64 | } 65 | 66 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 67 | -------------------------------------------------------------------------------- /src/linters/black.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { parseErrorsFromDiff } = require("../utils/diff"); 4 | const { initLintResult } = require("../utils/lint-result"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://black.readthedocs.io 10 | */ 11 | class Black { 12 | static get name() { 13 | return "Black"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that Python is installed (required to execute Black) 23 | if (!(await commandExists("python"))) { 24 | throw new Error("Python is not installed"); 25 | } 26 | 27 | // Verify that Black is installed 28 | try { 29 | run(`${prefix} black --version`, { dir }); 30 | } catch (err) { 31 | throw new Error(`${this.name} is not installed`); 32 | } 33 | } 34 | 35 | /** 36 | * Runs the linting program and returns the command output 37 | * @param {string} dir - Directory to run the linter in 38 | * @param {string[]} extensions - File extensions which should be linted 39 | * @param {string} args - Additional arguments to pass to the linter 40 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 41 | * @param {string} prefix - Prefix to the lint command 42 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 43 | */ 44 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 45 | const files = `^.*\\.(${extensions.join("|")})$`; 46 | const fixArg = fix ? "" : "--check --diff"; 47 | return run(`${prefix} black ${fixArg} --include "${files}" ${args} "."`, { 48 | dir, 49 | ignoreErrors: true, 50 | }); 51 | } 52 | 53 | /** 54 | * Parses the output of the lint command. Determines the success of the lint process and the 55 | * severity of the identified code style violations 56 | * @param {string} dir - Directory in which the linter has been run 57 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 58 | * @returns {LintResult} - Parsed lint result 59 | */ 60 | static parseOutput(dir, output) { 61 | const lintResult = initLintResult(); 62 | lintResult.error = parseErrorsFromDiff(output.stdout); 63 | lintResult.isSuccess = output.status === 0; 64 | return lintResult; 65 | } 66 | } 67 | 68 | module.exports = Black; 69 | -------------------------------------------------------------------------------- /src/linters/oitnb.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { parseErrorsFromDiff } = require("../utils/diff"); 4 | const { initLintResult } = require("../utils/lint-result"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://pypi.org/project/oitnb/ 10 | */ 11 | class Oitnb { 12 | static get name() { 13 | return "oitnb"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that Python is installed (required to execute oitnb) 23 | if (!(await commandExists("python"))) { 24 | throw new Error("Python is not installed"); 25 | } 26 | 27 | // Verify that oitnb is installed 28 | try { 29 | run(`${prefix} oitnb --version`, { dir }); 30 | } catch (err) { 31 | throw new Error(`${this.name} is not installed`); 32 | } 33 | } 34 | 35 | /** 36 | * Runs the linting program and returns the command output 37 | * @param {string} dir - Directory to run the linter in 38 | * @param {string[]} extensions - File extensions which should be linted 39 | * @param {string} args - Additional arguments to pass to the linter 40 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 41 | * @param {string} prefix - Prefix to the lint command 42 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 43 | */ 44 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 45 | const files = `^.*\\.(${extensions.join("|")})$`; 46 | const fixArg = fix ? "" : "--check --diff"; 47 | return run(`${prefix} oitnb ${fixArg} --include "${files}" ${args} "."`, { 48 | dir, 49 | ignoreErrors: true, 50 | }); 51 | } 52 | 53 | /** 54 | * Parses the output of the lint command. Determines the success of the lint process and the 55 | * severity of the identified code style violations 56 | * @param {string} dir - Directory in which the linter has been run 57 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 58 | * @returns {LintResult} - Parsed lint result 59 | */ 60 | static parseOutput(dir, output) { 61 | const lintResult = initLintResult(); 62 | lintResult.error = parseErrorsFromDiff(output.stdout); 63 | lintResult.isSuccess = output.status === 0; 64 | return lintResult; 65 | } 66 | } 67 | 68 | module.exports = Oitnb; 69 | -------------------------------------------------------------------------------- /test/linters/params/swiftlint.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const Swiftlint = require("../../../src/linters/swiftlint"); 4 | 5 | const testName = "swiftlint"; 6 | const linter = Swiftlint; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["swift"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const stdoutFile1 = `${join( 14 | dir, 15 | "file1.swift", 16 | )}:5:1: warning: Vertical Whitespace Violation: Limit vertical whitespace to a single empty line; currently 2 (vertical_whitespace)`; 17 | const stdoutFile2 = `${join( 18 | dir, 19 | "file2.swift", 20 | )}:2:22: error: Trailing Semicolon Violation: Lines should not have trailing semicolons. (trailing_semicolon)`; 21 | return { 22 | // Expected output of the linting function 23 | cmdOutput: { 24 | // SwiftLint exit codes: 25 | // - 0: No errors 26 | // - 1: Usage or system error 27 | // - 2: Style violations of severity "Error" 28 | // - 3: No style violations of severity "Error", but severity "Warning" with --strict 29 | status: 2, 30 | stdoutParts: [stdoutFile1, stdoutFile2], 31 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 32 | }, 33 | // Expected output of the parsing function 34 | lintResult: { 35 | isSuccess: false, 36 | warning: [ 37 | { 38 | path: "file1.swift", 39 | firstLine: 5, 40 | lastLine: 5, 41 | message: 42 | "Vertical Whitespace Violation: Limit vertical whitespace to a single empty line. Currently 2. (vertical_whitespace)", 43 | }, 44 | ], 45 | error: [ 46 | { 47 | path: "file2.swift", 48 | firstLine: 2, 49 | lastLine: 2, 50 | message: 51 | "Trailing Semicolon Violation: Lines should not have trailing semicolons. (trailing_semicolon)", 52 | }, 53 | ], 54 | }, 55 | }; 56 | } 57 | 58 | // Linting with auto-fixing 59 | function getFixParams(dir) { 60 | const stdoutFile1 = `${join(dir, "file1.swift")}:4:1 Corrected Vertical Whitespace`; 61 | const stdoutFile2 = `${join(dir, "file2.swift")}:2:22 Corrected Trailing Semicolon`; 62 | return { 63 | // Expected output of the linting function 64 | cmdOutput: { 65 | // SwiftLint exit codes: 66 | // - 0: No errors 67 | // - 1: Usage or system error 68 | // - 2: Style violations of severity "Error" 69 | // - 3: No style violations of severity "Error", but severity "Warning" with --strict 70 | status: 0, 71 | stdoutParts: [stdoutFile1, stdoutFile2], 72 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 73 | }, 74 | // Expected output of the parsing function 75 | lintResult: { 76 | isSuccess: true, 77 | warning: [], 78 | error: [], 79 | }, 80 | }; 81 | } 82 | 83 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 84 | -------------------------------------------------------------------------------- /src/linters/swiftlint.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: (warning|error): (.*)$/gm; 6 | 7 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 8 | 9 | /** 10 | * https://github.com/realm/SwiftLint 11 | */ 12 | class SwiftLint { 13 | static get name() { 14 | return "SwiftLint"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that SwiftLint is installed 24 | if (!(await commandExists("swiftlint"))) { 25 | throw new Error(`${this.name} is not installed`); 26 | } 27 | } 28 | 29 | /** 30 | * Runs the linting program and returns the command output 31 | * @param {string} dir - Directory to run the linter in 32 | * @param {string[]} extensions - File extensions which should be linted 33 | * @param {string} args - Additional arguments to pass to the linter 34 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 35 | * @param {string} prefix - Prefix to the lint command 36 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 37 | */ 38 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 39 | if (extensions.length !== 1 || extensions[0] !== "swift") { 40 | throw new Error(`${this.name} error: File extensions are not configurable`); 41 | } 42 | 43 | const fixArg = fix ? "--fix" : ""; 44 | return run(`${prefix} swiftlint ${fixArg} ${args}`, { 45 | dir, 46 | ignoreErrors: true, 47 | }); 48 | } 49 | 50 | /** 51 | * Parses the output of the lint command. Determines the success of the lint process and the 52 | * severity of the identified code style violations 53 | * @param {string} dir - Directory in which the linter has been run 54 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 55 | * @returns {LintResult} - Parsed lint result 56 | */ 57 | static parseOutput(dir, output) { 58 | const lintResult = initLintResult(); 59 | lintResult.isSuccess = output.status === 0; 60 | 61 | const matches = output.stdout.matchAll(PARSE_REGEX); 62 | for (const match of matches) { 63 | const [_, pathFull, line, level, message] = match; 64 | const path = pathFull.substring(dir.length + 1); 65 | const lineNr = parseInt(line, 10); 66 | lintResult[level].push({ 67 | path, 68 | firstLine: lineNr, 69 | lastLine: lineNr, 70 | message, 71 | }); 72 | } 73 | 74 | return lintResult; 75 | } 76 | } 77 | 78 | module.exports = SwiftLint; 79 | -------------------------------------------------------------------------------- /test/linters/params/rustfmt.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const RustFmt = require("../../../src/linters/rustfmt"); 4 | 5 | const testName = "rustfmt"; 6 | const linter = RustFmt; 7 | const commandPrefix = ""; 8 | const args = "-- --color=never"; 9 | const extensions = ["rs"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | let localDir = dir; 14 | if (process.platform === "win32") { 15 | // See https://github.com/rust-lang/rust/issues/42869 cargo fmt will output UNC paths 16 | localDir = `\\\\?\\${localDir}`; 17 | } 18 | const stdoutFile1 = `Diff in ${join( 19 | localDir, 20 | "src", 21 | "foo.rs", 22 | )} at line 2:\n //!\n //!\n //! This should push the error start line down past 1\n-use std::time::{SystemTime, Duration};\n+use std::time::{Duration, SystemTime};\n \n pub fn delta() -> Duration {\n- let start = SystemTime::now(); let delta = start.elapsed().unwrap();\n- delta\n+ let start = SystemTime::now();\n+ let delta = start.elapsed().unwrap();\n+ delta\n }\n `; 23 | const stdoutFile2 = `Diff in ${join( 24 | localDir, 25 | "src", 26 | "main.rs", 27 | )} at line 1:\n mod foo;\n-fn main() {let delta = foo::delta(); println!("Time delta is {delta:?}");}\n+fn main() {\n+ let delta = foo::delta();\n+ println!("Time delta is {delta:?}");\n+}`; 28 | return { 29 | // Expected output of the linting function 30 | cmdOutput: { 31 | status: 1, 32 | stdout: `${stdoutFile1}\n${stdoutFile2}`, 33 | }, 34 | // Expected output of the parsing function 35 | lintResult: { 36 | isSuccess: false, 37 | warning: [], 38 | error: [ 39 | { 40 | path: `${join("src", "foo.rs")}`, 41 | firstLine: 2, 42 | lastLine: 2, 43 | message: `\n //!\n //!\n //! This should push the error start line down past 1\n-use std::time::{SystemTime, Duration};\n+use std::time::{Duration, SystemTime};\n \n pub fn delta() -> Duration {\n- let start = SystemTime::now(); let delta = start.elapsed().unwrap();\n- delta\n+ let start = SystemTime::now();\n+ let delta = start.elapsed().unwrap();\n+ delta\n }\n \n`, 44 | }, 45 | { 46 | path: `${join("src", "main.rs")}`, 47 | firstLine: 1, 48 | lastLine: 1, 49 | message: `\n mod foo;\n-fn main() {let delta = foo::delta(); println!("Time delta is {delta:?}");}\n+fn main() {\n+ let delta = foo::delta();\n+ println!("Time delta is {delta:?}");\n+}`, 50 | }, 51 | ], 52 | }, 53 | }; 54 | } 55 | 56 | // Linting with auto-fixing 57 | function getFixParams(dir) { 58 | return { 59 | // Expected output of the linting function 60 | cmdOutput: { 61 | status: 0, 62 | }, 63 | // Expected output of the parsing function 64 | lintResult: { 65 | isSuccess: true, 66 | warning: [], 67 | error: [], 68 | }, 69 | }; 70 | } 71 | 72 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 73 | -------------------------------------------------------------------------------- /src/linters/autopep8.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { parseErrorsFromDiff } = require("../utils/diff"); 4 | const { initLintResult } = require("../utils/lint-result"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://github.com/hhatto/autopep8 10 | */ 11 | class Autopep8 { 12 | static get name() { 13 | return "Autopep8"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that Python is installed (required to execute Autopep8) 23 | if (!(await commandExists("python"))) { 24 | throw new Error("Python is not installed"); 25 | } 26 | 27 | // Verify that Autopep8 is installed 28 | try { 29 | run(`${prefix} autopep8 --version`, { dir }); 30 | } catch (err) { 31 | throw new Error(`${this.name} is not installed`); 32 | } 33 | } 34 | 35 | /** 36 | * Runs the linting program and returns the command output 37 | * @param {string} dir - Directory to run the linter in 38 | * @param {string[]} extensions - File extensions which should be linted 39 | * @param {string} args - Additional arguments to pass to the linter 40 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 41 | * @param {string} prefix - Prefix to the lint command 42 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 43 | */ 44 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 45 | if (extensions.length !== 1 || extensions[0] !== "py") { 46 | throw new Error(`${this.name} error: File extensions are not configurable`); 47 | } 48 | const fixArg = fix ? "-i" : "-d --exit-code"; 49 | const output = run(`${prefix} autopep8 ${fixArg} ${args} -r "."`, { 50 | dir, 51 | ignoreErrors: true, 52 | }); 53 | 54 | // Slashes can be different depending on OS 55 | output.stdout = output.stdout.replace(/^(---|\+\+\+) (original|fixed)\/\.[\\/]/gm, "$1 "); 56 | 57 | return output; 58 | } 59 | 60 | /** 61 | * Parses the output of the lint command. Determines the success of the lint process and the 62 | * severity of the identified code style violations 63 | * @param {string} dir - Directory in which the linter has been run 64 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 65 | * @returns {LintResult} - Parsed lint result 66 | */ 67 | static parseOutput(dir, output) { 68 | const lintResult = initLintResult(); 69 | lintResult.error = parseErrorsFromDiff(output.stdout); 70 | lintResult.isSuccess = output.status === 0; 71 | return lintResult; 72 | } 73 | } 74 | 75 | module.exports = Autopep8; 76 | -------------------------------------------------------------------------------- /src/linters/golint.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { run } = require("../utils/action"); 4 | const commandExists = require("../utils/command-exists"); 5 | const { initLintResult } = require("../utils/lint-result"); 6 | const { capitalizeFirstLetter } = require("../utils/string"); 7 | 8 | const PARSE_REGEX = /^(.+):([0-9]+):[0-9]+: (.+)$/gm; 9 | 10 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 11 | 12 | /** 13 | * https://github.com/golang/lint 14 | */ 15 | class Golint { 16 | static get name() { 17 | return "golint"; 18 | } 19 | 20 | /** 21 | * Verifies that all required programs are installed. Throws an error if programs are missing 22 | * @param {string} dir - Directory to run the linting program in 23 | * @param {string} prefix - Prefix to the lint command 24 | */ 25 | static async verifySetup(dir, prefix = "") { 26 | // Verify that golint is installed 27 | if (!(await commandExists("golint"))) { 28 | throw new Error(`${this.name} is not installed`); 29 | } 30 | } 31 | 32 | /** 33 | * Runs the linting program and returns the command output 34 | * @param {string} dir - Directory to run the linter in 35 | * @param {string[]} extensions - File extensions which should be linted 36 | * @param {string} args - Additional arguments to pass to the linter 37 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 38 | * @param {string} prefix - Prefix to the lint command 39 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 40 | */ 41 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 42 | if (extensions.length !== 1 || extensions[0] !== "go") { 43 | throw new Error(`${this.name} error: File extensions are not configurable`); 44 | } 45 | if (fix) { 46 | core.warning(`${this.name} does not support auto-fixing`); 47 | } 48 | 49 | return run(`${prefix} golint -set_exit_status ${args} "."`, { 50 | dir, 51 | ignoreErrors: true, 52 | }); 53 | } 54 | 55 | /** 56 | * Parses the output of the lint command. Determines the success of the lint process and the 57 | * severity of the identified code style violations 58 | * @param {string} dir - Directory in which the linter has been run 59 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 60 | * @returns {LintResult} - Parsed lint result 61 | */ 62 | static parseOutput(dir, output) { 63 | const lintResult = initLintResult(); 64 | lintResult.isSuccess = output.status === 0; 65 | 66 | const matches = output.stdout.matchAll(PARSE_REGEX); 67 | for (const match of matches) { 68 | const [_, path, line, text] = match; 69 | const lineNr = parseInt(line, 10); 70 | lintResult.error.push({ 71 | path, 72 | firstLine: lineNr, 73 | lastLine: lineNr, 74 | message: capitalizeFirstLetter(text), 75 | }); 76 | } 77 | 78 | return lintResult; 79 | } 80 | } 81 | 82 | module.exports = Golint; 83 | -------------------------------------------------------------------------------- /src/linters/rustfmt.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 6 | 7 | const PARSE_REGEX = /([\s\S]*?) at line (\d*):$([\s\S]*)/m; 8 | 9 | /** 10 | * https://github.com/rust-lang/rustfmt 11 | */ 12 | class RustFmt { 13 | static get name() { 14 | return "rustfmt"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that cargo format is installed 24 | if (!(await commandExists("cargo-fmt"))) { 25 | throw new Error("Cargo format is not installed"); 26 | } 27 | } 28 | 29 | /** 30 | * Runs the linting program and returns the command output 31 | * @param {string} dir - Directory to run the linter in 32 | * @param {string[]} extensions - File extensions which should be linted 33 | * @param {string} args - Additional arguments to pass to the linter 34 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 35 | * @param {string} prefix - Prefix to the lint command 36 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 37 | */ 38 | static lint(dir, extensions, args = "-- --color=never", fix = false, prefix = "") { 39 | if (extensions.length !== 1 || extensions[0] !== "rs") { 40 | throw new Error(`${this.name} error: File extensions are not configurable`); 41 | } 42 | const fixArg = fix ? "" : "--check"; 43 | return run(`${prefix} cargo fmt ${fixArg} ${args}`, { 44 | dir, 45 | ignoreErrors: true, 46 | }); 47 | } 48 | 49 | /** 50 | * Parses the output of the lint command. Determines the success of the lint process and the 51 | * severity of the identified code style violations 52 | * @param {string} dir - Directory in which the linter has been run 53 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 54 | * @returns {LintResult} - Parsed lint result 55 | */ 56 | static parseOutput(dir, output) { 57 | const lintResult = initLintResult(); 58 | lintResult.isSuccess = output.status === 0; 59 | if (!output.stdout) { 60 | return lintResult; 61 | } 62 | 63 | const diffs = output.stdout.split(/^Diff in /gm).slice(1); 64 | for (const diff of diffs) { 65 | const [_, pathFull, line, message] = diff.match(PARSE_REGEX); 66 | // Split on dir works for windows UNC paths, the substring strips out the 67 | // left over '/' or '\\' 68 | const path = pathFull.split(dir)[1].substring(1); 69 | const lineNr = parseInt(line, 10); 70 | lintResult.error.push({ 71 | path, 72 | firstLine: lineNr, 73 | lastLine: lineNr, 74 | message, 75 | }); 76 | } 77 | 78 | return lintResult; 79 | } 80 | } 81 | 82 | module.exports = RustFmt; 83 | -------------------------------------------------------------------------------- /src/linters/swift-format-lockwood.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: \w+: \((\w+)\) (.*)\.$/gm; 6 | 7 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 8 | 9 | /** 10 | * https://github.com/nicklockwood/SwiftFormat 11 | */ 12 | class SwiftFormatLockwood { 13 | static get name() { 14 | return "SwiftFormat"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that SwiftFormat is installed 24 | if (!(await commandExists("swiftformat"))) { 25 | throw new Error(`${this.name} is not installed`); 26 | } 27 | } 28 | 29 | /** 30 | * Runs the linting program and returns the command output 31 | * @param {string} dir - Directory to run the linter in 32 | * @param {string[]} extensions - File extensions which should be linted 33 | * @param {string} args - Additional arguments to pass to the linter 34 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 35 | * @param {string} prefix - Prefix to the lint command 36 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 37 | */ 38 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 39 | if (extensions.length !== 1 || extensions[0] !== "swift") { 40 | throw new Error(`${this.name} error: File extensions are not configurable`); 41 | } 42 | 43 | const fixArg = fix ? "" : "--lint"; 44 | return run(`${prefix} swiftformat ${fixArg} ${args} "."`, { 45 | dir, 46 | ignoreErrors: true, 47 | }); 48 | } 49 | 50 | /** 51 | * Parses the output of the lint command. Determines the success of the lint process and the 52 | * severity of the identified code style violations 53 | * @param {string} dir - Directory in which the linter has been run 54 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 55 | * @returns {LintResult} - Parsed lint result 56 | */ 57 | static parseOutput(dir, output) { 58 | const lintResult = initLintResult(); 59 | lintResult.isSuccess = output.status === 0; 60 | 61 | const matches = output.stderr.matchAll(PARSE_REGEX); 62 | for (const match of matches) { 63 | const [_, pathFull, line, rule, message] = match; 64 | const path = pathFull.substring(dir.length + 1); 65 | const lineNr = parseInt(line, 10); 66 | // SwiftFormat only seems to use the "warning" level, which this action will therefore 67 | // categorize as errors 68 | lintResult.error.push({ 69 | path, 70 | firstLine: lineNr, 71 | lastLine: lineNr, 72 | message: `${message} (${rule})`, 73 | }); 74 | } 75 | 76 | return lintResult; 77 | } 78 | } 79 | 80 | module.exports = SwiftFormatLockwood; 81 | -------------------------------------------------------------------------------- /test/linters/params/stylelint.js: -------------------------------------------------------------------------------- 1 | const Stylelint = require("../../../src/linters/stylelint"); 2 | const { joinDoubleBackslash } = require("../../test-utils"); 3 | 4 | const testName = "stylelint"; 5 | const linter = Stylelint; 6 | const args = ""; 7 | const commandPrefix = ""; 8 | const extensions = ["css", "sass", "scss"]; 9 | 10 | // Linting without auto-fixing 11 | function getLintParams(dir) { 12 | const stdoutFile1 = `{"source":"${joinDoubleBackslash( 13 | dir, 14 | "file1.css", 15 | )}","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[{"line":2,"column":14,"endLine":2,"endColumn":15,"rule":"no-extra-semicolons","severity":"warning","text":"Unexpected extra semicolon (no-extra-semicolons)"}]}`; 16 | const stdoutFile2 = `{"source":"${joinDoubleBackslash( 17 | dir, 18 | "file2.scss", 19 | )}","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":1,"column":6,"endLine":1,"endColumn":8,"rule":"block-no-empty","severity":"error","text":"Unexpected empty block (block-no-empty)"}]}`; 20 | return { 21 | // Expected output of the linting function 22 | cmdOutput: { 23 | status: 2, // stylelint exits with the highest severity index found (warning = 1, error = 2) 24 | stdoutParts: [stdoutFile1, stdoutFile2], 25 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 26 | }, 27 | // Expected output of the parsing function 28 | lintResult: { 29 | isSuccess: false, 30 | warning: [ 31 | { 32 | path: "file1.css", 33 | firstLine: 2, 34 | lastLine: 2, 35 | message: "Unexpected extra semicolon (no-extra-semicolons)", 36 | }, 37 | ], 38 | error: [ 39 | { 40 | path: "file2.scss", 41 | firstLine: 1, 42 | lastLine: 1, 43 | message: "Unexpected empty block (block-no-empty)", 44 | }, 45 | ], 46 | }, 47 | }; 48 | } 49 | 50 | // Linting with auto-fixing 51 | function getFixParams(dir) { 52 | const stdoutFile1 = `{"source":"${joinDoubleBackslash( 53 | dir, 54 | "file1.css", 55 | )}","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]}`; 56 | const stdoutFile2 = `{"source":"${joinDoubleBackslash( 57 | dir, 58 | "file2.scss", 59 | )}","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":1,"column":6,"endLine":1,"endColumn":8,"rule":"block-no-empty","severity":"error","text":"Unexpected empty block (block-no-empty)"}]}`; 60 | return { 61 | // Expected output of the linting function 62 | cmdOutput: { 63 | status: 2, // stylelint exits with the highest severity index found (warning = 1, error = 2) 64 | stdoutParts: [stdoutFile1, stdoutFile2], 65 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 66 | }, 67 | // Expected output of the parsing function 68 | lintResult: { 69 | isSuccess: false, 70 | warning: [], 71 | error: [ 72 | { 73 | path: "file2.scss", 74 | firstLine: 1, 75 | lastLine: 1, 76 | message: "Unexpected empty block (block-no-empty)", 77 | }, 78 | ], 79 | }, 80 | }; 81 | } 82 | 83 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 84 | -------------------------------------------------------------------------------- /src/linters/prettier.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | const { getNpmBinCommand } = require("../utils/npm/get-npm-bin-command"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://prettier.io 10 | */ 11 | class Prettier { 12 | static get name() { 13 | return "Prettier"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that NPM is installed (required to execute Prettier) 23 | if (!(await commandExists("npm"))) { 24 | throw new Error("NPM is not installed"); 25 | } 26 | 27 | // Verify that Prettier is installed 28 | const commandPrefix = prefix || getNpmBinCommand(dir); 29 | try { 30 | run(`${commandPrefix} prettier -v`, { dir }); 31 | } catch (err) { 32 | throw new Error(`${this.name} is not installed`); 33 | } 34 | } 35 | 36 | /** 37 | * Runs the linting program and returns the command output 38 | * @param {string} dir - Directory to run the linter in 39 | * @param {string[]} extensions - File extensions which should be linted 40 | * @param {string} args - Additional arguments to pass to the linter 41 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 42 | * @param {string} prefix - Prefix to the lint command 43 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 44 | */ 45 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 46 | const files = 47 | extensions.length === 1 ? `**/*.${extensions[0]}` : `**/*.{${extensions.join(",")}}`; 48 | const fixArg = fix ? "--write" : "--list-different"; 49 | const commandPrefix = prefix || getNpmBinCommand(dir); 50 | return run(`${commandPrefix} prettier ${fixArg} --no-color ${args} "${files}"`, { 51 | dir, 52 | ignoreErrors: true, 53 | }); 54 | } 55 | 56 | /** 57 | * Parses the output of the lint command. Determines the success of the lint process and the 58 | * severity of the identified code style violations 59 | * @param {string} dir - Directory in which the linter has been run 60 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 61 | * @returns {LintResult} - Parsed lint result 62 | */ 63 | static parseOutput(dir, output) { 64 | const lintResult = initLintResult(); 65 | lintResult.isSuccess = output.status === 0; 66 | if (lintResult.isSuccess || !output) { 67 | return lintResult; 68 | } 69 | 70 | const paths = output.stdout.split(/\r?\n/); 71 | lintResult.error = paths.map((path) => ({ 72 | path, 73 | firstLine: 1, 74 | lastLine: 1, 75 | message: 76 | "There are issues with this file's formatting, please run Prettier to fix the errors", 77 | })); 78 | 79 | return lintResult; 80 | } 81 | } 82 | 83 | module.exports = Prettier; 84 | -------------------------------------------------------------------------------- /src/linters/clang-format.js: -------------------------------------------------------------------------------- 1 | const glob = require("glob"); 2 | const { Shescape } = require("shescape"); 3 | 4 | const { run } = require("../utils/action"); 5 | const commandExists = require("../utils/command-exists"); 6 | const { initLintResult } = require("../utils/lint-result"); 7 | 8 | const { quoteAll } = new Shescape({ shell: false }); 9 | 10 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 11 | 12 | /** 13 | * https://clang.llvm.org/docs/ClangFormat.html 14 | */ 15 | class ClangFormat { 16 | static get name() { 17 | return "clang_format"; 18 | } 19 | 20 | /** 21 | * Verifies that all required programs are installed. Throws an error if programs are missing 22 | * @param {string} dir - Directory to run the linting program in 23 | * @param {string} prefix - Prefix to the lint command 24 | */ 25 | static async verifySetup(dir, prefix = "") { 26 | if (!(await commandExists("clang-format"))) { 27 | throw new Error("clang-format is not installed"); 28 | } 29 | } 30 | 31 | /** 32 | * Runs the linting program and returns the command output 33 | * @param {string} dir - Directory to run the linter in 34 | * @param {string[]} extensions - File extensions which should be linted 35 | * @param {string} args - Additional arguments to pass to the linter 36 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 37 | * @param {string} prefix - Prefix to the lint command 38 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 39 | */ 40 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 41 | const pattern = 42 | extensions.length === 1 ? `**/*.${extensions[0]}` : `**/*.{${extensions.join(",")}}`; 43 | const files = glob.sync(pattern, { cwd: dir, nodir: true }); 44 | const escapedFiles = quoteAll(files).join(" "); 45 | const fixArg = fix ? "-i" : "--dry-run"; 46 | return run(`${prefix} clang-format ${fixArg} -Werror ${args} ${escapedFiles}`, { 47 | dir, 48 | ignoreErrors: true, 49 | }); 50 | } 51 | 52 | /** 53 | * Parses the output of the lint command. Determines the success of the lint process and the 54 | * severity of the identified code style violations 55 | * @param {string} dir - Directory in which the linter has been run 56 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 57 | * @returns {LintResult} - Parsed lint result 58 | */ 59 | static parseOutput(dir, output) { 60 | const lintResult = initLintResult(); 61 | lintResult.isSuccess = output.status === 0; 62 | if (lintResult.isSuccess || !output) { 63 | return lintResult; 64 | } 65 | 66 | const lines = output.stderr.split(/\r?\n/); 67 | lintResult.error = lines.flatMap((line) => { 68 | const matched = line.match(/^(.*):(\d+):\d+: error: (.*)$/); 69 | if (!matched) { 70 | return []; 71 | } 72 | const lineNumber = parseInt(matched.at(2), 10); 73 | return { 74 | path: matched.at(1), 75 | firstLine: lineNumber, 76 | lastLine: lineNumber, 77 | message: matched.at(3), 78 | }; 79 | }); 80 | 81 | return lintResult; 82 | } 83 | } 84 | 85 | module.exports = ClangFormat; 86 | -------------------------------------------------------------------------------- /test/linters/params/php-codesniffer.js: -------------------------------------------------------------------------------- 1 | const PHPCodeSniffer = require("../../../src/linters/php-codesniffer"); 2 | 3 | const testName = "php-codesniffer"; 4 | const linter = PHPCodeSniffer; 5 | const args = ""; 6 | const commandPrefix = ""; 7 | const extensions = ["php"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdoutFile1 = `"file1.php":{"errors":0,"warnings":1,"messages":[{"message":"A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it should execute logic with side effects, but should not do both. The first symbol is defined on line 3 and the first side effect is on line 8.","source":"PSR1.Files.SideEffects.FoundWithSymbols","severity":5,"fixable":false,"type":"WARNING","line":1,"column":1}]}`; 12 | const stdoutFile2 = `"file2.php":{"errors":2,"warnings":0,"messages":[{"message":"Opening brace of a class must be on the line after the definition","source":"PSR2.Classes.ClassDeclaration.OpenBraceNewLine","severity":5,"fixable":true,"type":"ERROR","line":5,"column":17},{"message":"A closing tag is not permitted at the end of a PHP file","source":"PSR2.Files.ClosingTag.NotAllowed","severity":5,"fixable":true,"type":"ERROR","line":10,"column":1}]}`; 13 | // Files on macOS are not sorted. 14 | const stdout = 15 | process.platform === "darwin" 16 | ? `{"totals":{"errors":2,"warnings":1,"fixable":2},"files":{${stdoutFile2},${stdoutFile1}}}` 17 | : `{"totals":{"errors":2,"warnings":1,"fixable":2},"files":{${stdoutFile1},${stdoutFile2}}}`; 18 | return { 19 | // Expected output of the linting function 20 | cmdOutput: { 21 | // PHP_CodeSniffer exit codes: 22 | // - 0: No errors found. 23 | // - 1: Errors found, but none of them can be fixed by PHPCBF. 24 | // - 2: Errors found, and some can be fixed by PHPCBF. 25 | status: 2, 26 | stdoutParts: [stdoutFile1, stdoutFile2], 27 | stdout, 28 | }, 29 | // Expected output of the parsing function 30 | lintResult: { 31 | isSuccess: false, 32 | warning: [ 33 | { 34 | path: "file1.php", 35 | firstLine: 1, 36 | lastLine: 1, 37 | message: 38 | "A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it should execute logic with side effects, but should not do both. The first symbol is defined on line 3 and the first side effect is on line 8 (PSR1.Files.SideEffects.FoundWithSymbols)", 39 | }, 40 | ], 41 | error: [ 42 | { 43 | path: "file2.php", 44 | firstLine: 5, 45 | lastLine: 5, 46 | message: 47 | "Opening brace of a class must be on the line after the definition (PSR2.Classes.ClassDeclaration.OpenBraceNewLine)", 48 | }, 49 | { 50 | path: "file2.php", 51 | firstLine: 10, 52 | lastLine: 10, 53 | message: 54 | "A closing tag is not permitted at the end of a PHP file (PSR2.Files.ClosingTag.NotAllowed)", 55 | }, 56 | ], 57 | }, 58 | }; 59 | } 60 | 61 | // Linting with auto-fixing 62 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 63 | 64 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 65 | -------------------------------------------------------------------------------- /src/linters/dotnet-format.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | const PARSE_REGEX = /^(.*)\(([0-9]+),([0-9]+)\): (warning|error) (.*) \[.*$/gm; 6 | 7 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 8 | 9 | /** 10 | * https://github.com/dotnet/format 11 | */ 12 | class DotnetFormat { 13 | static get name() { 14 | return "dotnet_format"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that dotnet is installed (required to execute dotnet format) 24 | if (!(await commandExists("dotnet"))) { 25 | throw new Error(".NET SDK is not installed"); 26 | } 27 | 28 | // Verify that dotnet-format is installed 29 | try { 30 | run(`${prefix} dotnet format --version`, { dir }); 31 | } catch (err) { 32 | throw new Error(`${this.name} is not installed`); 33 | } 34 | } 35 | 36 | /** 37 | * Runs the linting program and returns the command output 38 | * @param {string} dir - Directory to run the linter in 39 | * @param {string[]} extensions - File extensions which should be linted 40 | * @param {string} args - Additional arguments to pass to the linter 41 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 42 | * @param {string} prefix - Prefix to the lint command 43 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 44 | */ 45 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 46 | if (extensions.length !== 1 || extensions[0] !== "cs") { 47 | throw new Error(`${this.name} error: File extensions are not configurable`); 48 | } 49 | 50 | const fixArg = fix ? "" : "--verify-no-changes"; 51 | return run(`${prefix} dotnet format ${fixArg} ${args}`, { 52 | dir, 53 | ignoreErrors: true, 54 | }); 55 | } 56 | 57 | /** 58 | * Parses the output of the lint command. Determines the success of the lint process and the 59 | * severity of the identified code style violations 60 | * @param {string} dir - Directory in which the linter has been run 61 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 62 | * @returns {LintResult} - Parsed lint result 63 | */ 64 | static parseOutput(dir, output) { 65 | const lintResult = initLintResult(); 66 | lintResult.isSuccess = output.status === 0; 67 | 68 | const matches = output.stderr.matchAll(PARSE_REGEX); 69 | for (const match of matches) { 70 | const [_line, pathFull, line, _column, level, message] = match; 71 | const path = pathFull.substring(dir.length + 1); 72 | const lineNr = parseInt(line, 10); 73 | lintResult[level].push({ 74 | path, 75 | firstLine: lineNr, 76 | lastLine: lineNr, 77 | message: `${message}`, 78 | }); 79 | } 80 | 81 | return lintResult; 82 | } 83 | } 84 | 85 | module.exports = DotnetFormat; 86 | -------------------------------------------------------------------------------- /test/linters/params/pylint.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require("os"); 2 | 3 | const Pylint = require("../../../src/linters/pylint"); 4 | 5 | const testName = "pylint"; 6 | const linter = Pylint; 7 | const args = ""; 8 | const commandPrefix = ""; 9 | const extensions = ["py"]; 10 | 11 | // Linting without auto-fixing 12 | function getLintParams(dir) { 13 | const stdoutFile1 = `file1.py:1:0: C0114: Missing module docstring (missing-module-docstring)${EOL}file1.py:1:0: C0115: Missing class docstring (missing-class-docstring)${EOL}file1.py:1:0: C0103: Class name "animal" doesn't conform to PascalCase naming style (invalid-name)${EOL}file1.py:1:0: R0903: Too few public methods (0/2) (too-few-public-methods)`; 14 | const stdoutFile2 = `file2.py:1:0: C0114: Missing module docstring (missing-module-docstring)${EOL}file2.py:1:0: C0103: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name)${EOL}file2.py:2:0: C0103: Constant name "b" doesn't conform to UPPER_CASE naming style (invalid-name)${EOL}file2.py:3:0: C0103: Constant name "c" doesn't conform to UPPER_CASE naming style (invalid-name)`; 15 | return { 16 | // Expected output of the linting function 17 | cmdOutput: { 18 | status: 24, // 1 refactor message (exit code 8) and 7 convention messages (exit code 16) 19 | stdoutParts: [stdoutFile1, stdoutFile2], 20 | stdout: `${stdoutFile1}${EOL}${stdoutFile2}`, 21 | }, 22 | // Expected output of the parsing function 23 | lintResult: { 24 | isSuccess: false, 25 | warning: [], 26 | error: [ 27 | { 28 | path: "file1.py", 29 | firstLine: 1, 30 | lastLine: 1, 31 | message: "Missing module docstring (missing-module-docstring, C0114)", 32 | }, 33 | { 34 | path: "file1.py", 35 | firstLine: 1, 36 | lastLine: 1, 37 | message: "Missing class docstring (missing-class-docstring, C0115)", 38 | }, 39 | { 40 | path: "file1.py", 41 | firstLine: 1, 42 | lastLine: 1, 43 | message: 44 | 'Class name "animal" doesn\'t conform to PascalCase naming style (invalid-name, C0103)', 45 | }, 46 | { 47 | path: "file1.py", 48 | firstLine: 1, 49 | lastLine: 1, 50 | message: "Too few public methods (0/2) (too-few-public-methods, R0903)", 51 | }, 52 | { 53 | path: "file2.py", 54 | firstLine: 1, 55 | lastLine: 1, 56 | message: "Missing module docstring (missing-module-docstring, C0114)", 57 | }, 58 | { 59 | path: "file2.py", 60 | firstLine: 1, 61 | lastLine: 1, 62 | message: 63 | 'Constant name "a" doesn\'t conform to UPPER_CASE naming style (invalid-name, C0103)', 64 | }, 65 | { 66 | path: "file2.py", 67 | firstLine: 2, 68 | lastLine: 2, 69 | message: 70 | 'Constant name "b" doesn\'t conform to UPPER_CASE naming style (invalid-name, C0103)', 71 | }, 72 | { 73 | path: "file2.py", 74 | firstLine: 3, 75 | lastLine: 3, 76 | message: 77 | 'Constant name "c" doesn\'t conform to UPPER_CASE naming style (invalid-name, C0103)', 78 | }, 79 | ], 80 | }, 81 | }; 82 | } 83 | 84 | // Linting with auto-fixing 85 | const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect 86 | 87 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 88 | -------------------------------------------------------------------------------- /src/linters/swift-format-official.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | const PARSE_REGEX = /^(.*):([0-9]+):([0-9]+): (warning|error): (.*)$/gm; 6 | 7 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 8 | 9 | /** 10 | * https://github.com/apple/swift-format 11 | */ 12 | class SwiftFormatOfficial { 13 | static get name() { 14 | return "swift-format"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that swift-format is installed. 24 | if (!(await commandExists("swift-format"))) { 25 | throw new Error(`${this.name} is not installed`); 26 | } 27 | } 28 | 29 | /** 30 | * Runs the linting program and returns the command output 31 | * @param {string} dir - Directory to run the linter in 32 | * @param {string[]} extensions - File extensions which should be linted 33 | * @param {string} args - Additional arguments to pass to the linter 34 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 35 | * @param {string} prefix - Prefix to the lint command 36 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 37 | */ 38 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 39 | if (extensions.length !== 1 || extensions[0] !== "swift") { 40 | throw new Error(`${this.name} error: File extensions are not configurable`); 41 | } 42 | 43 | const mode = fix ? "format -i" : "lint"; 44 | return run(`${prefix} swift-format ${mode} ${args} --recursive "."`, { 45 | dir, 46 | ignoreErrors: true, 47 | }); 48 | } 49 | 50 | /** 51 | * Parses the output of the lint command. Determines the success of the lint process and the 52 | * severity of the identified code style violations 53 | * @param {string} dir - Directory in which the linter has been run 54 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 55 | * @returns {LintResult} - Parsed lint result 56 | */ 57 | static parseOutput(dir, output) { 58 | const lintResult = initLintResult(); 59 | 60 | const matches = output.stderr.matchAll(PARSE_REGEX); 61 | for (const match of matches) { 62 | const [_line, pathFull, line, _column, _level, message] = match; 63 | const path = pathFull.substring(dir.length + 1); 64 | const lineNr = parseInt(line, 10); 65 | // swift-format only seems to use the "warning" level, which this action will therefore 66 | // categorize as errors 67 | lintResult.error.push({ 68 | path, 69 | firstLine: lineNr, 70 | lastLine: lineNr, 71 | message: `${message}`, 72 | }); 73 | } 74 | 75 | // Since 0.50300.0 swift-format exits with 0 even if there are formatting issues. Therefore, 76 | // this function determines the success of the linting process based on the number of parsed 77 | // errors. 78 | lintResult.isSuccess = lintResult.error.length === 0; 79 | 80 | return lintResult; 81 | } 82 | } 83 | 84 | module.exports = SwiftFormatOfficial; 85 | -------------------------------------------------------------------------------- /src/linters/flake8.js: -------------------------------------------------------------------------------- 1 | const { sep } = require("path"); 2 | 3 | const core = require("@actions/core"); 4 | 5 | const { run } = require("../utils/action"); 6 | const commandExists = require("../utils/command-exists"); 7 | const { initLintResult } = require("../utils/lint-result"); 8 | const { capitalizeFirstLetter } = require("../utils/string"); 9 | 10 | const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: (\w*) (.*)$/gm; 11 | 12 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 13 | 14 | /** 15 | * http://flake8.pycqa.org 16 | */ 17 | class Flake8 { 18 | static get name() { 19 | return "Flake8"; 20 | } 21 | 22 | /** 23 | * Verifies that all required programs are installed. Throws an error if programs are missing 24 | * @param {string} dir - Directory to run the linting program in 25 | * @param {string} prefix - Prefix to the lint command 26 | */ 27 | static async verifySetup(dir, prefix = "") { 28 | // Verify that Python is installed (required to execute Flake8) 29 | if (!(await commandExists("python"))) { 30 | throw new Error("Python is not installed"); 31 | } 32 | 33 | // Verify that Flake8 is installed 34 | try { 35 | run(`${prefix} flake8 --version`, { dir }); 36 | } catch (err) { 37 | throw new Error(`${this.name} is not installed`); 38 | } 39 | } 40 | 41 | /** 42 | * Runs the linting program and returns the command output 43 | * @param {string} dir - Directory to run the linter in 44 | * @param {string[]} extensions - File extensions which should be linted 45 | * @param {string} args - Additional arguments to pass to the linter 46 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 47 | * @param {string} prefix - Prefix to the lint command 48 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 49 | */ 50 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 51 | if (fix) { 52 | core.warning(`${this.name} does not support auto-fixing`); 53 | } 54 | 55 | const files = extensions.map((ext) => `"**${sep}*.${ext}"`).join(","); 56 | return run(`${prefix} flake8 --filename ${files} ${args}`, { 57 | dir, 58 | ignoreErrors: true, 59 | }); 60 | } 61 | 62 | /** 63 | * Parses the output of the lint command. Determines the success of the lint process and the 64 | * severity of the identified code style violations 65 | * @param {string} dir - Directory in which the linter has been run 66 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 67 | * @returns {LintResult} - Parsed lint result 68 | */ 69 | static parseOutput(dir, output) { 70 | const lintResult = initLintResult(); 71 | lintResult.isSuccess = output.status === 0; 72 | 73 | const matches = output.stdout.matchAll(PARSE_REGEX); 74 | for (const match of matches) { 75 | const [_, pathFull, line, rule, text] = match; 76 | const leadingSep = `.${sep}`; 77 | let path = pathFull; 78 | if (path.startsWith(leadingSep)) { 79 | path = path.substring(2); // Remove "./" or ".\" from start of path 80 | } 81 | const lineNr = parseInt(line, 10); 82 | lintResult.error.push({ 83 | path, 84 | firstLine: lineNr, 85 | lastLine: lineNr, 86 | message: `${capitalizeFirstLetter(text)} (${rule})`, 87 | }); 88 | } 89 | 90 | return lintResult; 91 | } 92 | } 93 | 94 | module.exports = Flake8; 95 | -------------------------------------------------------------------------------- /test/linters/params/rubocop.js: -------------------------------------------------------------------------------- 1 | const RuboCop = require("../../../src/linters/rubocop"); 2 | 3 | const testName = "rubocop"; 4 | const linter = RuboCop; 5 | const args = ""; 6 | const commandPrefix = "bundle exec"; 7 | const extensions = ["rb"]; 8 | 9 | // Linting without auto-fixing 10 | function getLintParams(dir) { 11 | const stdoutFile1 = `{"path":"file1.rb","offenses":[{"severity":"convention","message":"Redundant \`return\` detected.","cop_name":"Style/RedundantReturn","corrected":false,"correctable":true,"location":{"start_line":5,"start_column":3,"last_line":5,"last_column":8,"length":6,"line":5,"column":3}}]}`; 12 | const stdoutFile2 = `{"path":"file2.rb","offenses":[{"severity":"warning","message":"Useless assignment to variable - \`x\`.","cop_name":"Lint/UselessAssignment","corrected":false,"correctable":false,"location":{"start_line":4,"start_column":1,"last_line":4,"last_column":1,"length":1,"line":4,"column":1}}]}`; 13 | return { 14 | // Expected output of the linting function 15 | cmdOutput: { 16 | status: 1, 17 | stdoutParts: [stdoutFile1, stdoutFile2], 18 | stdout: `{"metadata":{"rubocop_version":"0.93.0","ruby_engine":"ruby","ruby_version":"2.5.3","ruby_patchlevel":"105","ruby_platform":"x86_64-darwin18"},"files":[${stdoutFile1},${stdoutFile2}],"summary":{"offense_count":2,"target_file_count":2,"inspected_file_count":2}}`, 19 | }, 20 | // Expected output of the parsing function 21 | lintResult: { 22 | isSuccess: false, 23 | warning: [ 24 | { 25 | path: "file1.rb", 26 | firstLine: 5, 27 | lastLine: 5, 28 | message: "Redundant `return` detected (Style/RedundantReturn)", 29 | }, 30 | { 31 | path: "file2.rb", 32 | firstLine: 4, 33 | lastLine: 4, 34 | message: "Useless assignment to variable - `x` (Lint/UselessAssignment)", 35 | }, 36 | ], 37 | error: [], 38 | }, 39 | }; 40 | } 41 | 42 | // Linting with auto-fixing 43 | function getFixParams(dir) { 44 | const stdoutFile1 = `{"path":"file1.rb","offenses":[{"severity":"convention","message":"Redundant \`return\` detected.","cop_name":"Style/RedundantReturn","corrected":true,"correctable":true,"location":{"start_line":5,"start_column":3,"last_line":5,"last_column":8,"length":6,"line":5,"column":3}}]}`; 45 | const stdoutFile2 = `{"path":"file2.rb","offenses":[{"severity":"warning","message":"Useless assignment to variable - \`x\`.","cop_name":"Lint/UselessAssignment","corrected":false,"correctable":false,"location":{"start_line":4,"start_column":1,"last_line":4,"last_column":1,"length":1,"line":4,"column":1}}]}`; 46 | return { 47 | // Expected output of the linting function 48 | cmdOutput: { 49 | status: 1, 50 | stdoutParts: [stdoutFile1, stdoutFile2], 51 | stdout: `{"metadata":{"rubocop_version":"0.93.0","ruby_engine":"ruby","ruby_version":"2.5.3","ruby_patchlevel":"105","ruby_platform":"x86_64-darwin18"},"files":[${stdoutFile1},${stdoutFile2}],"summary":{"offense_count":2,"target_file_count":2,"inspected_file_count":2}}`, 52 | }, 53 | // Expected output of the parsing function 54 | lintResult: { 55 | isSuccess: false, 56 | warning: [ 57 | { 58 | path: "file2.rb", 59 | firstLine: 4, 60 | lastLine: 4, 61 | message: "Useless assignment to variable - `x` (Lint/UselessAssignment)", 62 | }, 63 | ], 64 | error: [], 65 | }, 66 | }; 67 | } 68 | 69 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 70 | -------------------------------------------------------------------------------- /src/linters/pylint.js: -------------------------------------------------------------------------------- 1 | const { sep } = require("path"); 2 | 3 | const core = require("@actions/core"); 4 | 5 | const { run } = require("../utils/action"); 6 | const commandExists = require("../utils/command-exists"); 7 | const { initLintResult } = require("../utils/lint-result"); 8 | const { capitalizeFirstLetter } = require("../utils/string"); 9 | 10 | const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: (\w*): (.*) (.*)$/gm; 11 | 12 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 13 | 14 | /* 15 | https://pylint.pycqa.org 16 | */ 17 | class Pylint { 18 | static get name() { 19 | return "Pylint"; 20 | } 21 | 22 | /** 23 | * Verifies that all required programs are installed. Throws an error if programs are missing 24 | * @param {string} dir - Directory to run the linting program in 25 | * @param {string} prefix - Prefix to the lint command 26 | */ 27 | static async verifySetup(dir, prefix = "") { 28 | // Verify that Python is installed (required to execute Pylint) 29 | if (!(await commandExists("python"))) { 30 | throw new Error("Python is not installed"); 31 | } 32 | 33 | // Verify that Pylint is installed 34 | try { 35 | run(`${prefix} pylint --version`, { dir }); 36 | } catch (err) { 37 | throw new Error(`${this.name} is not installed`); 38 | } 39 | } 40 | 41 | /** 42 | * Runs the linting program and returns the command output 43 | * @param {string} dir - Directory to run the linter in 44 | * @param {string[]} extensions - File extensions which should be linted 45 | * @param {string} args - Additional arguments to pass to the linter 46 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 47 | * @param {string} prefix - Prefix to the lint command 48 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 49 | */ 50 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 51 | if (extensions.length !== 1 || extensions[0] !== "py") { 52 | throw new Error(`${this.name} error: File extensions are not configurable`); 53 | } 54 | if (fix) { 55 | core.warning(`${this.name} does not support auto-fixing`); 56 | } 57 | 58 | return run(`${prefix} pylint --recursive=y "." ${args}`, { 59 | dir, 60 | ignoreErrors: true, 61 | }); 62 | } 63 | 64 | /** 65 | * Parses the output of the lint command. Determines the success of the lint process and the 66 | * severity of the identified code style violations 67 | * @param {string} dir - Directory in which the linter has been run 68 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 69 | * @returns {LintResult} - Parsed lint result 70 | */ 71 | static parseOutput(dir, output) { 72 | const lintResult = initLintResult(); 73 | lintResult.isSuccess = output.status === 0; 74 | 75 | const matches = output.stdout.matchAll(PARSE_REGEX); 76 | for (const match of matches) { 77 | const [_, pathFull, line, ruleId, text, rule] = match; 78 | const leadingSep = `.${sep}`; 79 | let path = pathFull; 80 | if (path.startsWith(leadingSep)) { 81 | path = path.substring(2); // Remove "./" or ".\" from start of path 82 | } 83 | const lineNr = parseInt(line, 10); 84 | lintResult.error.push({ 85 | path, 86 | firstLine: lineNr, 87 | lastLine: lineNr, 88 | message: `${capitalizeFirstLetter(text)} (${rule.replace(/[)(]/g, "")}, ${ruleId})`, 89 | }); 90 | } 91 | 92 | return lintResult; 93 | } 94 | } 95 | 96 | module.exports = Pylint; 97 | -------------------------------------------------------------------------------- /src/git.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { run } = require("./utils/action"); 4 | 5 | /** @typedef {import('./github/context').GithubContext} GithubContext */ 6 | 7 | /** 8 | * Fetches and checks out the remote Git branch (if it exists, the fork repository will be used) 9 | * @param {GithubContext} context - Information about the GitHub 10 | */ 11 | function checkOutRemoteBranch(context) { 12 | if (context.repository.hasFork) { 13 | // Fork: Add fork repo as remote 14 | core.info(`Adding "${context.repository.forkName}" fork as remote with Git`); 15 | const cloneURl = new URL(context.repository.forkCloneUrl); 16 | cloneURl.username = context.actor; 17 | cloneURl.password = context.token; 18 | run(`git remote add fork ${cloneURl.toString()}`); 19 | } else { 20 | // No fork: Update remote URL to include auth information (so auto-fixes can be pushed) 21 | core.info(`Adding auth information to Git remote URL`); 22 | const cloneURl = new URL(context.repository.cloneUrl); 23 | cloneURl.username = context.actor; 24 | cloneURl.password = context.token; 25 | run(`git remote set-url origin ${cloneURl.toString()}`); 26 | } 27 | 28 | const remote = context.repository.hasFork ? "fork" : "origin"; 29 | 30 | // Fetch remote branch 31 | core.info(`Fetching remote branch "${context.branch}"`); 32 | run(`git fetch --no-tags --depth=1 ${remote} ${context.branch}`); 33 | 34 | // Switch to remote branch 35 | core.info(`Switching to the "${context.branch}" branch`); 36 | run(`git branch --force ${context.branch} --track ${remote}/${context.branch}`); 37 | run(`git checkout ${context.branch}`); 38 | } 39 | 40 | /** 41 | * Stages and commits all changes using Git 42 | * @param {string} message - Git commit message 43 | * @param {boolean} skipVerification - Skip Git verification 44 | */ 45 | function commitChanges(message, skipVerification) { 46 | core.info(`Committing changes`); 47 | run(`git commit -am "${message}"${skipVerification ? " --no-verify" : ""}`); 48 | } 49 | 50 | /** 51 | * Returns the SHA of the head commit 52 | * @returns {string} - Head SHA 53 | */ 54 | function getHeadSha() { 55 | const sha = run("git rev-parse HEAD").stdout; 56 | core.info(`SHA of last commit is "${sha}"`); 57 | return sha; 58 | } 59 | 60 | /** 61 | * Checks whether there are differences from HEAD 62 | * @returns {boolean} - Boolean indicating whether changes exist 63 | */ 64 | function hasChanges() { 65 | const output = run("git diff-index --name-status --exit-code HEAD --", { ignoreErrors: true }); 66 | const hasChangedFiles = output.status === 1; 67 | core.info(`${hasChangedFiles ? "Changes" : "No changes"} found with Git`); 68 | return hasChangedFiles; 69 | } 70 | 71 | /** 72 | * Pushes all changes to the remote repository 73 | * @param {boolean} skipVerification - Skip Git verification 74 | */ 75 | function pushChanges(skipVerification) { 76 | core.info("Pushing changes with Git"); 77 | run(`git push${skipVerification ? " --no-verify" : ""}`); 78 | } 79 | 80 | /** 81 | * Updates the global Git configuration with the provided information 82 | * @param {string} name - Git username 83 | * @param {string} email - Git email address 84 | */ 85 | function setUserInfo(name, email) { 86 | core.info(`Setting Git user information`); 87 | run(`git config --global user.name "${name}"`); 88 | run(`git config --global user.email "${email}"`); 89 | } 90 | 91 | module.exports = { 92 | checkOutRemoteBranch, 93 | commitChanges, 94 | getHeadSha, 95 | hasChanges, 96 | pushChanges, 97 | setUserInfo, 98 | }; 99 | -------------------------------------------------------------------------------- /src/linters/stylelint.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | const { getNpmBinCommand } = require("../utils/npm/get-npm-bin-command"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://stylelint.io 10 | */ 11 | class Stylelint { 12 | static get name() { 13 | return "stylelint"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that NPM is installed (required to execute stylelint) 23 | if (!(await commandExists("npm"))) { 24 | throw new Error("NPM is not installed"); 25 | } 26 | 27 | // Verify that stylelint is installed 28 | const commandPrefix = prefix || getNpmBinCommand(dir); 29 | try { 30 | run(`${commandPrefix} stylelint -v`, { dir }); 31 | } catch (err) { 32 | throw new Error(`${this.name} is not installed`); 33 | } 34 | } 35 | 36 | /** 37 | * Runs the linting program and returns the command output 38 | * @param {string} dir - Directory to run the linter in 39 | * @param {string[]} extensions - File extensions which should be linted 40 | * @param {string} args - Additional arguments to pass to the linter 41 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 42 | * @param {string} prefix - Prefix to the lint command 43 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 44 | */ 45 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 46 | const files = 47 | extensions.length === 1 ? `**/*.${extensions[0]}` : `**/*.{${extensions.join(",")}}`; 48 | const fixArg = fix ? "--fix" : ""; 49 | const commandPrefix = prefix || getNpmBinCommand(dir); 50 | return run( 51 | `${commandPrefix} stylelint --no-color --formatter json ${fixArg} ${args} "${files}"`, 52 | { 53 | dir, 54 | ignoreErrors: true, 55 | }, 56 | ); 57 | } 58 | 59 | /** 60 | * Parses the output of the lint command. Determines the success of the lint process and the 61 | * severity of the identified code style violations 62 | * @param {string} dir - Directory in which the linter has been run 63 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 64 | * @returns {LintResult} - Parsed lint result 65 | */ 66 | static parseOutput(dir, output) { 67 | const lintResult = initLintResult(); 68 | lintResult.isSuccess = output.status === 0; 69 | 70 | let outputJson; 71 | try { 72 | outputJson = JSON.parse(output.stdout); 73 | } catch (err) { 74 | throw Error( 75 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 76 | ); 77 | } 78 | 79 | for (const violation of outputJson) { 80 | const { source, warnings } = violation; 81 | const path = source.substring(dir.length + 1); 82 | for (const warning of warnings) { 83 | const { line, severity, text } = warning; 84 | if (severity in lintResult) { 85 | lintResult[severity].push({ 86 | path, 87 | firstLine: line, 88 | lastLine: line, 89 | message: text, 90 | }); 91 | } 92 | } 93 | } 94 | 95 | return lintResult; 96 | } 97 | } 98 | 99 | module.exports = Stylelint; 100 | -------------------------------------------------------------------------------- /src/linters/erblint.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { run } = require("../utils/action"); 4 | const commandExists = require("../utils/command-exists"); 5 | const { initLintResult } = require("../utils/lint-result"); 6 | const { removeTrailingPeriod } = require("../utils/string"); 7 | 8 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 9 | 10 | /** 11 | * https://https://github.com/Shopify/erb-lint 12 | */ 13 | class Erblint { 14 | static get name() { 15 | return "ERB Lint"; 16 | } 17 | 18 | /** 19 | * Verifies that all required programs are installed. Throws an error if programs are missing 20 | * @param {string} dir - Directory to run the linting program in 21 | * @param {string} prefix - Prefix to the lint command 22 | */ 23 | static async verifySetup(dir, prefix = "") { 24 | // Verify that Ruby is installed (required to execute erblint) 25 | if (!(await commandExists("ruby"))) { 26 | throw new Error("Ruby is not installed"); 27 | } 28 | // Verify that erblint is installed 29 | try { 30 | run(`${prefix} erblint -v`, { dir }); 31 | } catch (err) { 32 | throw new Error(`${this.name} is not installed`); 33 | } 34 | } 35 | 36 | /** 37 | * Runs the linting program and returns the command output 38 | * @param {string} dir - Directory to run the linter in 39 | * @param {string[]} extensions - File extensions which should be linted 40 | * @param {string} args - Additional arguments to pass to the linter 41 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 42 | * @param {string} prefix - Prefix to the lint command 43 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 44 | */ 45 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 46 | if (extensions.length !== 1 || extensions[0] !== "erb") { 47 | throw new Error(`${this.name} error: File extensions are not configurable`); 48 | } 49 | if (fix) { 50 | core.warning(`${this.name} does not support auto-fixing`); 51 | } 52 | 53 | return run(`${prefix} erblint --format json --lint-all ${args}`, { 54 | dir, 55 | ignoreErrors: true, 56 | }); 57 | } 58 | 59 | /** 60 | * Parses the output of the lint command. Determines the success of the lint process and the 61 | * severity of the identified code style violations 62 | * @param {string} dir - Directory in which the linter has been run 63 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 64 | * @returns {LintResult} - Parsed lint result 65 | */ 66 | static parseOutput(dir, output) { 67 | const lintResult = initLintResult(); 68 | lintResult.isSuccess = output.status === 0; 69 | 70 | let outputJson; 71 | try { 72 | outputJson = JSON.parse(output.stdout); 73 | } catch (err) { 74 | throw Error( 75 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 76 | ); 77 | } 78 | 79 | for (const file of outputJson.files) { 80 | const { path, offenses } = file; 81 | for (const offense of offenses) { 82 | const { message, linter, corrected, location } = offense; 83 | if (!corrected) { 84 | // ERB Lint does not provide severities in its JSON output 85 | lintResult.error.push({ 86 | path, 87 | firstLine: location.start_line, 88 | lastLine: location.last_line, 89 | message: `${removeTrailingPeriod(message)} (${linter})`, 90 | }); 91 | } 92 | } 93 | } 94 | 95 | return lintResult; 96 | } 97 | } 98 | 99 | module.exports = Erblint; 100 | -------------------------------------------------------------------------------- /src/linters/gofmt.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { parseErrorsFromDiff } = require("../utils/diff"); 4 | const { initLintResult } = require("../utils/lint-result"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | /** 9 | * https://golang.org/cmd/gofmt 10 | */ 11 | class Gofmt { 12 | static get name() { 13 | return "gofmt"; 14 | } 15 | 16 | /** 17 | * Verifies that all required programs are installed. Throws an error if programs are missing 18 | * @param {string} dir - Directory to run the linting program in 19 | * @param {string} prefix - Prefix to the lint command 20 | */ 21 | static async verifySetup(dir, prefix = "") { 22 | // Verify that gofmt is installed 23 | if (!(await commandExists("gofmt"))) { 24 | throw new Error(`${this.name} is not installed`); 25 | } 26 | } 27 | 28 | /** 29 | * Runs the linting program and returns the command output 30 | * @param {string} dir - Directory to run the linter in 31 | * @param {string[]} extensions - File extensions which should be linted 32 | * @param {string} args - Additional arguments to pass to the linter 33 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 34 | * @param {string} prefix - Prefix to the lint command 35 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 36 | */ 37 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 38 | if (extensions.length !== 1 || extensions[0] !== "go") { 39 | throw new Error(`${this.name} error: File extensions are not configurable`); 40 | } 41 | 42 | // -d: Display diffs instead of rewriting files 43 | // -e: Report all errors (not just the first 10 on different lines) 44 | // -s: Simplify code 45 | // -w: Write result to (source) file instead of stdout 46 | const fixArg = fix ? "-w" : "-d -e"; 47 | return run(`${prefix} gofmt -s ${fixArg} ${args} "."`, { 48 | dir, 49 | ignoreErrors: true, 50 | }); 51 | } 52 | 53 | /** 54 | * Parses the output of the lint command. Determines the success of the lint process and the 55 | * severity of the identified code style violations 56 | * @param {string} dir - Directory in which the linter has been run 57 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 58 | * @returns {LintResult} - Parsed lint result 59 | */ 60 | static parseOutput(dir, output) { 61 | const lintResult = initLintResult(); 62 | 63 | // The gofmt output lines starting with "diff" differ from the ones of tools like Git: 64 | // 65 | // - gofmt: "diff -u file-old.txt file-new.txt" 66 | // - Git: "diff --git a/file-old.txt b/file-new.txt" 67 | // 68 | // The diff parser relies on the "a/" and "b/" strings to be able to tell where file names 69 | // start. Without these strings, this would not be possible, because file names may include 70 | // spaces, which are not escaped in unified diffs. As a workaround, these lines are filtered out 71 | // from the gofmt diff so the diff parser can read the diff without errors 72 | const filteredOutput = output.stdout 73 | .split(/\r?\n/) 74 | .filter((line) => !line.startsWith("diff ")) 75 | .join("\n"); 76 | lintResult.error = parseErrorsFromDiff(filteredOutput); 77 | 78 | // gofmt exits with 0 even if there are formatting issues. Therefore, this function determines 79 | // the success of the linting process based on the number of parsed errors 80 | lintResult.isSuccess = lintResult.error.length === 0; 81 | 82 | return lintResult; 83 | } 84 | } 85 | 86 | module.exports = Gofmt; 87 | -------------------------------------------------------------------------------- /src/github/api.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { name: actionName } = require("../../package.json"); 4 | const request = require("../utils/request"); 5 | const { capitalizeFirstLetter } = require("../utils/string"); 6 | 7 | /** @typedef {import('./context').GithubContext} GithubContext */ 8 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 9 | 10 | /** 11 | * Creates a new check on GitHub which annotates the relevant commit with linting errors 12 | * @param {string} linterName - Name of the linter for which a check should be created 13 | * @param {string} sha - SHA of the commit which should be annotated 14 | * @param {GithubContext} context - Information about the GitHub repository and 15 | * action trigger event 16 | * @param {LintResult} lintResult - Parsed lint result 17 | * @param {boolean} neutralCheckOnWarning - Whether the check run should conclude as neutral if 18 | * there are only warnings 19 | * @param {string} summary - Summary for the GitHub check 20 | */ 21 | async function createCheck(linterName, sha, context, lintResult, neutralCheckOnWarning, summary) { 22 | let annotations = []; 23 | for (const level of ["error", "warning"]) { 24 | annotations = [ 25 | ...annotations, 26 | ...lintResult[level].map((result) => ({ 27 | path: result.path, 28 | start_line: result.firstLine, 29 | end_line: result.lastLine, 30 | annotation_level: level === "warning" ? "warning" : "failure", 31 | message: result.message, 32 | })), 33 | ]; 34 | } 35 | 36 | // Only use the first 50 annotations (limit for a single API request) 37 | if (annotations.length > 50) { 38 | core.info( 39 | `There are more than 50 errors/warnings from ${linterName}. Annotations are created for the first 50 issues only.`, 40 | ); 41 | annotations = annotations.slice(0, 50); 42 | } 43 | 44 | let conclusion; 45 | if (lintResult.isSuccess) { 46 | if (annotations.length > 0 && neutralCheckOnWarning) { 47 | conclusion = "neutral"; 48 | } else { 49 | conclusion = "success"; 50 | } 51 | } else { 52 | conclusion = "failure"; 53 | } 54 | 55 | const body = { 56 | name: linterName, 57 | head_sha: sha, 58 | conclusion, 59 | output: { 60 | title: capitalizeFirstLetter(summary), 61 | summary: `${linterName} found ${summary}`, 62 | annotations, 63 | }, 64 | }; 65 | try { 66 | core.info( 67 | `Creating GitHub check with ${conclusion} conclusion and ${annotations.length} annotations for ${linterName}…`, 68 | ); 69 | await request(`${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs`, { 70 | method: "POST", 71 | headers: { 72 | "Content-Type": "application/json", 73 | // "Accept" header is required to access Checks API during preview period 74 | Accept: "application/vnd.github.antiope-preview+json", 75 | Authorization: `Bearer ${context.token}`, 76 | "User-Agent": actionName, 77 | }, 78 | body, 79 | }); 80 | core.info(`${linterName} check created successfully`); 81 | } catch (err) { 82 | let errorMessage = err.message; 83 | if (err.data) { 84 | try { 85 | const errorData = JSON.parse(err.data); 86 | if (errorData.message) { 87 | errorMessage += `. ${errorData.message}`; 88 | } 89 | if (errorData.documentation_url) { 90 | errorMessage += ` ${errorData.documentation_url}`; 91 | } 92 | } catch (e) { 93 | // Ignore 94 | } 95 | } 96 | core.error(errorMessage); 97 | 98 | throw new Error(`Error trying to create GitHub check for ${linterName}: ${errorMessage}`); 99 | } 100 | } 101 | 102 | module.exports = { createCheck }; 103 | -------------------------------------------------------------------------------- /src/linters/php-codesniffer.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { run } = require("../utils/action"); 4 | const commandExists = require("../utils/command-exists"); 5 | const { initLintResult } = require("../utils/lint-result"); 6 | const { removeTrailingPeriod } = require("../utils/string"); 7 | 8 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 9 | 10 | /** 11 | * https://github.com/squizlabs/PHP_CodeSniffer 12 | */ 13 | class PHPCodeSniffer { 14 | static get name() { 15 | return "PHP_CodeSniffer"; 16 | } 17 | 18 | /** 19 | * Verifies that all required programs are installed. Throws an error if programs are missing 20 | * @param {string} dir - Directory to run the linting program in 21 | * @param {string} prefix - Prefix to the lint command 22 | */ 23 | static async verifySetup(dir, prefix = "") { 24 | // Verify that PHP is installed (required to execute phpcs) 25 | if (!(await commandExists("php"))) { 26 | throw new Error("PHP is not installed"); 27 | } 28 | 29 | // Verify that phpcs is installed 30 | try { 31 | run(`${prefix} phpcs --version`, { dir }); 32 | } catch (err) { 33 | throw new Error(`${this.name} is not installed`); 34 | } 35 | } 36 | 37 | /** 38 | * Runs the linting program and returns the command output 39 | * @param {string} dir - Directory to run the linter in 40 | * @param {string[]} extensions - File extensions which should be linted 41 | * @param {string} args - Additional arguments to pass to the linter 42 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 43 | * @param {string} prefix - Prefix to the lint command 44 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 45 | */ 46 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 47 | const extensionsArg = extensions.join(","); 48 | if (fix) { 49 | core.warning(`${this.name} does not support auto-fixing`); 50 | } 51 | 52 | return run(`${prefix} phpcs --extensions=${extensionsArg} --report=json -q ${args} "."`, { 53 | dir, 54 | ignoreErrors: true, 55 | }); 56 | } 57 | 58 | /** 59 | * Parses the output of the lint command. Determines the success of the lint process and the 60 | * severity of the identified code style violations 61 | * @param {string} dir - Directory in which the linter has been run 62 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 63 | * @returns {LintResult} - Parsed lint result 64 | */ 65 | static parseOutput(dir, output) { 66 | const lintResult = initLintResult(); 67 | lintResult.isSuccess = output.status === 0; 68 | 69 | let outputJson; 70 | try { 71 | outputJson = JSON.parse(output.stdout); 72 | } catch (err) { 73 | throw Error( 74 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 75 | ); 76 | } 77 | 78 | for (const [file, violations] of Object.entries(outputJson.files)) { 79 | const path = file.indexOf(dir) === 0 ? file.substring(dir.length + 1) : file; 80 | 81 | for (const msg of violations.messages) { 82 | const { line, message, source, type } = msg; 83 | 84 | const entry = { 85 | path, 86 | firstLine: line, 87 | lastLine: line, 88 | message: `${removeTrailingPeriod(message)} (${source})`, 89 | }; 90 | if (type === "WARNING") { 91 | lintResult.warning.push(entry); 92 | } else if (type === "ERROR") { 93 | lintResult.error.push(entry); 94 | } 95 | } 96 | } 97 | 98 | return lintResult; 99 | } 100 | } 101 | 102 | module.exports = PHPCodeSniffer; 103 | -------------------------------------------------------------------------------- /src/linters/tsc.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | 3 | const { run } = require("../utils/action"); 4 | const commandExists = require("../utils/command-exists"); 5 | const { initLintResult } = require("../utils/lint-result"); 6 | const { getNpmBinCommand } = require("../utils/npm/get-npm-bin-command"); 7 | const { removeTrailingPeriod } = require("../utils/string"); 8 | 9 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 10 | 11 | /** 12 | * https://www.typescriptlang.org/docs/handbook/compiler-options.html 13 | */ 14 | class TSC { 15 | static get name() { 16 | return "TypeScript"; 17 | } 18 | 19 | /** 20 | * Verifies that all required programs are installed. Throws an error if programs are missing 21 | * @param {string} dir - Directory to run the linting program in 22 | * @param {string} prefix - Prefix to the lint command 23 | */ 24 | static async verifySetup(dir, prefix = "") { 25 | // Verify that NPM is installed (required to execute ESLint) 26 | if (!(await commandExists("npm"))) { 27 | throw new Error("NPM is not installed"); 28 | } 29 | 30 | // Verify that ESLint is installed 31 | const commandPrefix = prefix || getNpmBinCommand(dir); 32 | try { 33 | run(`${commandPrefix} tsc -v`, { dir }); 34 | } catch (err) { 35 | throw new Error(`${this.name} is not installed`); 36 | } 37 | } 38 | 39 | /** 40 | * Runs the linting program and returns the command output 41 | * @param {string} dir - Directory to run the linter in 42 | * @param {string[]} extensions - File extensions which should be linted 43 | * @param {string} args - Additional arguments to pass to the linter 44 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 45 | * @param {string} prefix - Prefix to the lint command 46 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 47 | */ 48 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 49 | if (fix) { 50 | core.warning(`${this.name} does not support auto-fixing`); 51 | } 52 | 53 | const commandPrefix = prefix || getNpmBinCommand(dir); 54 | return run(`${commandPrefix} tsc --noEmit --pretty false ${args}`, { 55 | dir, 56 | ignoreErrors: true, 57 | }); 58 | } 59 | 60 | /** 61 | * Parses the output of the lint command. Determines the success of the lint process and the 62 | * severity of the identified code style violations 63 | * @param {string} dir - Directory in which the linter has been run 64 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 65 | * @returns {LintResult} - Parsed lint result 66 | */ 67 | static parseOutput(dir, output) { 68 | const lintResult = initLintResult(); 69 | lintResult.isSuccess = output.status === 0; 70 | 71 | // example: file1.ts(4,25): error TS7005: Variable 'str' implicitly has an 'any' type. 72 | const regex = /^(?.+)\((?\d+),(?\d+)\):\s(?\w+)\s(?.+)$/gm; 73 | 74 | const errors = []; 75 | const matches = output.stdout.matchAll(regex); 76 | 77 | for (const match of matches) { 78 | const { file, line, column, code, message } = match.groups; 79 | errors.push({ file, line, column, code, message }); 80 | } 81 | 82 | for (const error of errors) { 83 | const { file, line, message } = error; 84 | 85 | const entry = { 86 | path: file, 87 | firstLine: Number(line), 88 | lastLine: Number(line), 89 | message: `${removeTrailingPeriod(message)}`, 90 | }; 91 | 92 | lintResult.error.push(entry); 93 | } 94 | 95 | return lintResult; 96 | } 97 | } 98 | 99 | module.exports = TSC; 100 | -------------------------------------------------------------------------------- /src/linters/rubocop.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | const { removeTrailingPeriod } = require("../utils/string"); 5 | 6 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 7 | 8 | // Mapping of RuboCop severities to severities used for GitHub commit annotations 9 | const severityMap = { 10 | convention: "warning", 11 | refactor: "warning", 12 | warning: "warning", 13 | error: "error", 14 | fatal: "error", 15 | }; 16 | 17 | /** 18 | * https://rubocop.readthedocs.io 19 | */ 20 | class RuboCop { 21 | static get name() { 22 | return "RuboCop"; 23 | } 24 | 25 | /** 26 | * Verifies that all required programs are installed. Throws an error if programs are missing 27 | * @param {string} dir - Directory to run the linting program in 28 | * @param {string} prefix - Prefix to the lint command 29 | */ 30 | static async verifySetup(dir, prefix = "") { 31 | // Verify that Ruby is installed (required to execute RuboCop) 32 | if (!(await commandExists("ruby"))) { 33 | throw new Error("Ruby is not installed"); 34 | } 35 | // Verify that RuboCop is installed 36 | try { 37 | run(`${prefix} rubocop -v`, { dir }); 38 | } catch (err) { 39 | throw new Error(`${this.name} is not installed`); 40 | } 41 | } 42 | 43 | /** 44 | * Runs the linting program and returns the command output 45 | * @param {string} dir - Directory to run the linter in 46 | * @param {string[]} extensions - File extensions which should be linted 47 | * @param {string} args - Additional arguments to pass to the linter 48 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 49 | * @param {string} prefix - Prefix to the lint command 50 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 51 | */ 52 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 53 | if (extensions.length !== 1 || extensions[0] !== "rb") { 54 | throw new Error(`${this.name} error: File extensions are not configurable`); 55 | } 56 | 57 | const fixArg = fix ? "--auto-correct" : ""; 58 | return run(`${prefix} rubocop --format json ${fixArg} ${args}`, { 59 | dir, 60 | ignoreErrors: true, 61 | }); 62 | } 63 | 64 | /** 65 | * Parses the output of the lint command. Determines the success of the lint process and the 66 | * severity of the identified code style violations 67 | * @param {string} dir - Directory in which the linter has been run 68 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 69 | * @returns {LintResult} - Parsed lint result 70 | */ 71 | static parseOutput(dir, output) { 72 | const lintResult = initLintResult(); 73 | lintResult.isSuccess = output.status === 0; 74 | 75 | let outputJson; 76 | try { 77 | outputJson = JSON.parse(output.stdout); 78 | } catch (err) { 79 | throw Error( 80 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 81 | ); 82 | } 83 | 84 | for (const file of outputJson.files) { 85 | const { path, offenses } = file; 86 | for (const offense of offenses) { 87 | const { severity, message, cop_name: rule, corrected, location } = offense; 88 | if (!corrected) { 89 | const mappedSeverity = severityMap[severity] || "error"; 90 | lintResult[mappedSeverity].push({ 91 | path, 92 | firstLine: location.start_line, 93 | lastLine: location.last_line, 94 | message: `${removeTrailingPeriod(message)} (${rule})`, 95 | }); 96 | } 97 | } 98 | } 99 | 100 | return lintResult; 101 | } 102 | } 103 | 104 | module.exports = RuboCop; 105 | -------------------------------------------------------------------------------- /src/linters/eslint.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | const { getNpmBinCommand } = require("../utils/npm/get-npm-bin-command"); 5 | const { removeTrailingPeriod } = require("../utils/string"); 6 | 7 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 8 | 9 | /** 10 | * https://eslint.org 11 | */ 12 | class ESLint { 13 | static get name() { 14 | return "ESLint"; 15 | } 16 | 17 | /** 18 | * Verifies that all required programs are installed. Throws an error if programs are missing 19 | * @param {string} dir - Directory to run the linting program in 20 | * @param {string} prefix - Prefix to the lint command 21 | */ 22 | static async verifySetup(dir, prefix = "") { 23 | // Verify that NPM is installed (required to execute ESLint) 24 | if (!(await commandExists("npm"))) { 25 | throw new Error("NPM is not installed"); 26 | } 27 | 28 | // Verify that ESLint is installed 29 | const commandPrefix = prefix || getNpmBinCommand(dir); 30 | try { 31 | run(`${commandPrefix} eslint -v`, { dir }); 32 | } catch (err) { 33 | throw new Error(`${this.name} is not installed`); 34 | } 35 | } 36 | 37 | /** 38 | * Runs the linting program and returns the command output 39 | * @param {string} dir - Directory to run the linter in 40 | * @param {string[]} extensions - File extensions which should be linted 41 | * @param {string} args - Additional arguments to pass to the linter 42 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 43 | * @param {string} prefix - Prefix to the lint command 44 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 45 | */ 46 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 47 | const extensionsArg = extensions.map((ext) => `.${ext}`).join(","); 48 | const fixArg = fix ? "--fix" : ""; 49 | const commandPrefix = prefix || getNpmBinCommand(dir); 50 | return run( 51 | `${commandPrefix} eslint --ext ${extensionsArg} ${fixArg} --no-color --format json ${args} "."`, 52 | { 53 | dir, 54 | ignoreErrors: true, 55 | }, 56 | ); 57 | } 58 | 59 | /** 60 | * Parses the output of the lint command. Determines the success of the lint process and the 61 | * severity of the identified code style violations 62 | * @param {string} dir - Directory in which the linter has been run 63 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 64 | * @returns {LintResult} - Parsed lint result 65 | */ 66 | static parseOutput(dir, output) { 67 | const lintResult = initLintResult(); 68 | lintResult.isSuccess = output.status === 0; 69 | 70 | let outputJson; 71 | try { 72 | outputJson = JSON.parse(output.stdout); 73 | } catch (err) { 74 | throw Error( 75 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 76 | ); 77 | } 78 | 79 | for (const violation of outputJson) { 80 | const { filePath, messages } = violation; 81 | const path = filePath.substring(dir.length + 1); 82 | 83 | for (const msg of messages) { 84 | const { fatal, line, message, ruleId, severity } = msg; 85 | 86 | // Exit if a fatal ESLint error occurred 87 | if (fatal) { 88 | throw Error(`ESLint error: ${message}`); 89 | } 90 | 91 | const entry = { 92 | path, 93 | firstLine: line, 94 | lastLine: line, 95 | message: `${removeTrailingPeriod(message)} (${ruleId})`, 96 | }; 97 | if (severity === 1) { 98 | lintResult.warning.push(entry); 99 | } else if (severity === 2) { 100 | lintResult.error.push(entry); 101 | } 102 | } 103 | } 104 | 105 | return lintResult; 106 | } 107 | } 108 | 109 | module.exports = ESLint; 110 | -------------------------------------------------------------------------------- /src/linters/mypy.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { sep } = require("path"); 3 | 4 | const core = require("@actions/core"); 5 | 6 | const { run } = require("../utils/action"); 7 | const commandExists = require("../utils/command-exists"); 8 | const { initLintResult } = require("../utils/lint-result"); 9 | 10 | const PARSE_REGEX = /^(.*):([0-9]+): (\w*): (.*)$/gm; 11 | 12 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 13 | 14 | /** 15 | * https://mypy.readthedocs.io/en/stable/ 16 | */ 17 | class Mypy { 18 | static get name() { 19 | return "Mypy"; 20 | } 21 | 22 | /** 23 | * Verifies that all required programs are installed. Throws an error if programs are missing 24 | * @param {string} dir - Directory to run the linting program in 25 | * @param {string} prefix - Prefix to the lint command 26 | */ 27 | static async verifySetup(dir, prefix = "") { 28 | // Verify that Python is installed (required to execute Mypy) 29 | if (!(await commandExists("python"))) { 30 | throw new Error("Python is not installed"); 31 | } 32 | 33 | // Verify that Mypy is installed 34 | try { 35 | run(`${prefix} mypy --version`, { dir }); 36 | } catch (err) { 37 | throw new Error(`${this.name} is not installed`); 38 | } 39 | } 40 | 41 | /** 42 | * Runs the linting program and returns the command output 43 | * @param {string} dir - Directory to run the linter in 44 | * @param {string[]} extensions - File extensions which should be linted 45 | * @param {string} args - Additional arguments to pass to the linter 46 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 47 | * @param {string} prefix - Prefix to the lint command 48 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 49 | */ 50 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 51 | if (extensions.length !== 1 || extensions[0] !== "py") { 52 | throw new Error(`${this.name} error: File extensions are not configurable`); 53 | } 54 | if (fix) { 55 | core.warning(`${this.name} does not support auto-fixing`); 56 | } 57 | 58 | let specifiedPath = false; 59 | // Check if they passed a directory as an arg 60 | for (const arg of args.split(" ")) { 61 | if (fs.existsSync(arg)) { 62 | specifiedPath = true; 63 | break; 64 | } 65 | } 66 | let extraArgs = ""; 67 | if (!specifiedPath) { 68 | extraArgs = ` ${dir}`; 69 | } 70 | return run(`${prefix} mypy ${args}${extraArgs}`, { 71 | dir, 72 | ignoreErrors: true, 73 | }); 74 | } 75 | 76 | /** 77 | * Parses the output of the lint command. Determines the success of the lint process and the 78 | * severity of the identified code style violations 79 | * @param {string} dir - Directory in which the linter has been run 80 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 81 | * @returns {LintResult} - Parsed lint result 82 | */ 83 | static parseOutput(dir, output) { 84 | const lintResult = initLintResult(); 85 | lintResult.isSuccess = output.status === 0; 86 | 87 | const matches = output.stdout.matchAll(PARSE_REGEX); 88 | for (const match of matches) { 89 | const [_, pathFull, line, level, text] = match; 90 | const leadingSep = `.${sep}`; 91 | let path = pathFull; 92 | if (path.startsWith(leadingSep)) { 93 | path = path.substring(2); // Remove "./" or ".\" from start of path 94 | } 95 | const lineNr = parseInt(line, 10); 96 | const result = { 97 | path, 98 | firstLine: lineNr, 99 | lastLine: lineNr, 100 | message: text, 101 | }; 102 | if (level === "error") { 103 | lintResult.error.push(result); 104 | } else if (level === "warning") { 105 | lintResult.warning.push(result); 106 | } 107 | } 108 | 109 | return lintResult; 110 | } 111 | } 112 | 113 | module.exports = Mypy; 114 | -------------------------------------------------------------------------------- /test/github/api-responses/check-runs.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789, 3 | "node_id": "123456789", 4 | "head_sha": "0000000000000000000000000000000000000000", 5 | "external_id": "", 6 | "url": "https://api.github.com/repos/example/example/check-runs/123456789", 7 | "html_url": "https://github.com/example/example/runs/123456789", 8 | "details_url": "https://help.github.com/en/actions", 9 | "status": "completed", 10 | "conclusion": "failure", 11 | "started_at": "2019-01-01T00:00:00Z", 12 | "completed_at": "2019-01-01T00:00:00Z", 13 | "output": { 14 | "title": "eslint", 15 | "summary": "eslint found 123 errors", 16 | "text": null, 17 | "annotations_count": 1, 18 | "annotations_url": "https://api.github.com/repos/example/example/check-runs/123456789/annotations" 19 | }, 20 | "name": "eslint", 21 | "check_suite": { 22 | "id": 123456789 23 | }, 24 | "app": { 25 | "id": 123456789, 26 | "slug": "github-actions", 27 | "node_id": "123456789", 28 | "owner": { 29 | "login": "github", 30 | "id": 123456789, 31 | "node_id": "123456789", 32 | "avatar_url": "https://avatars1.githubusercontent.com/u/123456789?v=4", 33 | "gravatar_id": "", 34 | "url": "https://api.github.com/users/github", 35 | "html_url": "https://github.com/github", 36 | "followers_url": "https://api.github.com/users/github/followers", 37 | "following_url": "https://api.github.com/users/github/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/github/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/github/subscriptions", 41 | "organizations_url": "https://api.github.com/users/github/orgs", 42 | "repos_url": "https://api.github.com/users/github/repos", 43 | "events_url": "https://api.github.com/users/github/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/github/received_events", 45 | "type": "Organization", 46 | "site_admin": false 47 | }, 48 | "name": "GitHub Actions", 49 | "description": "Automate your workflow from idea to production", 50 | "external_url": "https://help.github.com/en/actions", 51 | "html_url": "https://github.com/apps/github-actions", 52 | "created_at": "2019-01-01T00:00:00Z", 53 | "updated_at": "2019-01-01T00:00:00Z", 54 | "permissions": { 55 | "app_config": "read", 56 | "checks": "write", 57 | "contents": "write", 58 | "deployments": "write", 59 | "issues": "write", 60 | "metadata": "read", 61 | "packages": "write", 62 | "pages": "write", 63 | "pull_requests": "write", 64 | "repository_hooks": "write", 65 | "repository_projects": "write", 66 | "statuses": "write", 67 | "vulnerability_alerts": "read" 68 | }, 69 | "events": [ 70 | "check_run", 71 | "check_suite", 72 | "create", 73 | "delete", 74 | "deployment", 75 | "deployment_status", 76 | "fork", 77 | "gollum", 78 | "issues", 79 | "issue_comment", 80 | "label", 81 | "milestone", 82 | "page_build", 83 | "project", 84 | "project_card", 85 | "project_column", 86 | "public", 87 | "pull_request", 88 | "pull_request_review", 89 | "pull_request_review_comment", 90 | "push", 91 | "registry_package", 92 | "release", 93 | "repository", 94 | "repository_dispatch", 95 | "status", 96 | "watch" 97 | ] 98 | }, 99 | "pull_requests": [ 100 | { 101 | "url": "https://api.github.com/repos/example/example/pulls/1", 102 | "id": 123456789, 103 | "number": 1, 104 | "head": { 105 | "ref": "example-patch-1", 106 | "sha": "0000000000000000000000000000000000000000", 107 | "repo": { 108 | "id": 123456789, 109 | "url": "https://api.github.com/repos/example/example", 110 | "name": "example" 111 | } 112 | }, 113 | "base": { 114 | "ref": "master", 115 | "sha": "0000000000000000000000000000000000000000", 116 | "repo": { 117 | "id": 123456789, 118 | "url": "https://api.github.com/repos/example/example", 119 | "name": "example" 120 | } 121 | } 122 | } 123 | ] 124 | } 125 | -------------------------------------------------------------------------------- /src/linters/clippy.js: -------------------------------------------------------------------------------- 1 | const { run } = require("../utils/action"); 2 | const commandExists = require("../utils/command-exists"); 3 | const { initLintResult } = require("../utils/lint-result"); 4 | 5 | /** @typedef {import('../utils/lint-result').LintResult} LintResult */ 6 | 7 | /** 8 | * https://rust-lang.github.io/rust-clippy/ 9 | */ 10 | class Clippy { 11 | static get name() { 12 | return "clippy"; 13 | } 14 | 15 | /** 16 | * Verifies that all required programs are installed. Throws an error if programs are missing 17 | * @param {string} dir - Directory to run the linting program in 18 | * @param {string} prefix - Prefix to the lint command 19 | */ 20 | static async verifySetup(dir, prefix = "") { 21 | // Verify that cargo is installed (required to execute clippy) 22 | if (!(await commandExists("cargo"))) { 23 | throw new Error("cargo is not installed"); 24 | } 25 | 26 | // Verify that clippy is installed 27 | try { 28 | run(`${prefix} cargo clippy --version`, { dir }); 29 | } catch (err) { 30 | throw new Error(`${this.name} is not installed`); 31 | } 32 | } 33 | 34 | /** 35 | * Runs the linting program and returns the command output 36 | * @param {string} dir - Directory to run the linter in 37 | * @param {string[]} extensions - File extensions which should be linted 38 | * @param {string} args - Additional arguments to pass to the linter 39 | * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically 40 | * @param {string} prefix - Prefix to the lint command 41 | * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command 42 | */ 43 | static lint(dir, extensions, args = "", fix = false, prefix = "") { 44 | if (extensions.length !== 1 || extensions[0] !== "rs") { 45 | throw new Error(`${this.name} error: File extensions are not configurable`); 46 | } 47 | 48 | // clippy will throw an error if `--allow-dirty` is used when `--fix` isn't. 49 | // in order to have tests run consistently and to help out users we remove `--allow-dirty` 50 | // when not in fix 51 | const localArgs = fix ? args : args.replace("--allow-dirty", ""); 52 | 53 | const fixArg = fix ? "--fix" : ""; 54 | return run(`${prefix} cargo clippy ${fixArg} --message-format json ${localArgs}`, { 55 | dir, 56 | ignoreErrors: true, 57 | }); 58 | } 59 | 60 | /** 61 | * Parses the output of the lint command. Determines the success of the lint process and the 62 | * severity of the identified code style violations 63 | * @param {string} dir - Directory in which the linter has been run 64 | * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command 65 | * @returns {LintResult} - Parsed lint result 66 | */ 67 | static parseOutput(dir, output) { 68 | const lintResult = initLintResult(); 69 | 70 | const lines = output.stdout.split("\n").map((line) => { 71 | let parsedLine; 72 | try { 73 | let normalizedLine = line; 74 | if (process.platform === "win32") { 75 | normalizedLine = line.replace(/\\/gi, "\\\\"); 76 | } 77 | parsedLine = JSON.parse(normalizedLine); 78 | } catch (err) { 79 | throw Error( 80 | `Error parsing ${this.name} JSON output: ${err.message}. Output: "${output.stdout}"`, 81 | ); 82 | } 83 | return parsedLine; 84 | }); 85 | 86 | lines.forEach((line) => { 87 | if (line.reason === "compiler-message") { 88 | if (line.message.level === "warning") { 89 | const { code, message, spans } = line.message; 90 | // don't add the message counting the warnings 91 | if (code !== null) { 92 | lintResult.warning.push({ 93 | path: spans[0].file_name, 94 | firstLine: spans[0].line_start, 95 | lastLine: spans[0].line_end, 96 | message, 97 | }); 98 | } 99 | } else if (line.message.level === "error") { 100 | const { code, message, spans } = line.message; 101 | // don't add the message counting the errors 102 | if (code !== null) { 103 | lintResult.warning.push({ 104 | path: spans[0].file_name, 105 | firstLine: spans[0].line_start, 106 | lastLine: spans[0].line_end, 107 | message, 108 | }); 109 | } 110 | } 111 | } 112 | }); 113 | 114 | lintResult.isSuccess = 115 | output.status === 0 && lintResult.warning.length === 0 && lintResult.error.length === 0; 116 | 117 | return lintResult; 118 | } 119 | } 120 | 121 | module.exports = Clippy; 122 | -------------------------------------------------------------------------------- /test/linters/params/eslint.js: -------------------------------------------------------------------------------- 1 | const ESLint = require("../../../src/linters/eslint"); 2 | const { joinDoubleBackslash } = require("../../test-utils"); 3 | 4 | const testName = "eslint"; 5 | const linter = ESLint; 6 | const args = ""; 7 | const commandPrefix = ""; 8 | const extensions = ["js"]; 9 | 10 | // Linting without auto-fixing 11 | function getLintParams(dir) { 12 | const stdoutFile1 = `{"filePath":"${joinDoubleBackslash( 13 | dir, 14 | "file1.js", 15 | )}","messages":[{"ruleId":"prefer-const","severity":2,"message":"'str' is never reassigned. Use 'const' instead.","line":1,"column":5,"nodeType":"Identifier","messageId":"useConst","endLine":1,"endColumn":8,"fix":{"range":[0,18],"text":"const str = 'world';"}},{"ruleId":"no-warning-comments","severity":1,"message":"Unexpected 'todo' comment: 'TODO: Change something'.","line":5,"column":31,"nodeType":"Line","messageId":"unexpectedComment","endLine":5,"endColumn":56}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":1,"fixableWarningCount":0,"source":"let str = 'world'; // \\"prefer-const\\" warning\\n\\nfunction main() {\\n\\t// \\"no-warning-comments\\" error\\n\\tconsole.log('hello ' + str); // TODO: Change something\\n}\\n\\nmain();\\n","usedDeprecatedRules":[]}`; 16 | const stdoutFile2 = `{"filePath":"${joinDoubleBackslash( 17 | dir, 18 | "file2.js", 19 | )}","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'str' is assigned a value but never used.","line":1,"column":7,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":10}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const str = 'Hello world'; // \\"no-unused-vars\\" error\\n","usedDeprecatedRules":[]}`; 20 | return { 21 | // Expected output of the linting function 22 | cmdOutput: { 23 | status: 1, 24 | stdoutParts: [stdoutFile1, stdoutFile2], 25 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 26 | }, 27 | // Expected output of the parsing function 28 | lintResult: { 29 | isSuccess: false, 30 | warning: [ 31 | { 32 | path: "file1.js", 33 | firstLine: 5, 34 | lastLine: 5, 35 | message: "Unexpected 'todo' comment: 'TODO: Change something' (no-warning-comments)", 36 | }, 37 | ], 38 | error: [ 39 | { 40 | path: "file1.js", 41 | firstLine: 1, 42 | lastLine: 1, 43 | message: "'str' is never reassigned. Use 'const' instead (prefer-const)", 44 | }, 45 | { 46 | path: "file2.js", 47 | firstLine: 1, 48 | lastLine: 1, 49 | message: "'str' is assigned a value but never used (no-unused-vars)", 50 | }, 51 | ], 52 | }, 53 | }; 54 | } 55 | 56 | // Linting with auto-fixing 57 | function getFixParams(dir) { 58 | const stdoutFile1 = `{"filePath":"${joinDoubleBackslash( 59 | dir, 60 | "file1.js", 61 | )}","messages":[{"ruleId":"no-warning-comments","severity":1,"message":"Unexpected 'todo' comment: 'TODO: Change something'.","line":5,"column":31,"nodeType":"Line","messageId":"unexpectedComment","endLine":5,"endColumn":56}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"output":"const str = 'world'; // \\"prefer-const\\" warning\\n\\nfunction main() {\\n\\t// \\"no-warning-comments\\" error\\n\\tconsole.log('hello ' + str); // TODO: Change something\\n}\\n\\nmain();\\n","usedDeprecatedRules":[]}`; 62 | const stdoutFile2 = `{"filePath":"${joinDoubleBackslash( 63 | dir, 64 | "file2.js", 65 | )}","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'str' is assigned a value but never used.","line":1,"column":7,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":10}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const str = 'Hello world'; // \\"no-unused-vars\\" error\\n","usedDeprecatedRules":[]}`; 66 | return { 67 | // Expected output of the linting function 68 | cmdOutput: { 69 | status: 1, 70 | stdoutParts: [stdoutFile1, stdoutFile2], 71 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 72 | }, 73 | // Expected output of the parsing function 74 | lintResult: { 75 | isSuccess: false, 76 | warning: [ 77 | { 78 | path: "file1.js", 79 | firstLine: 5, 80 | lastLine: 5, 81 | message: "Unexpected 'todo' comment: 'TODO: Change something' (no-warning-comments)", 82 | }, 83 | ], 84 | error: [ 85 | { 86 | path: "file2.js", 87 | firstLine: 1, 88 | lastLine: 1, 89 | message: "'str' is assigned a value but never used (no-unused-vars)", 90 | }, 91 | ], 92 | }, 93 | }; 94 | } 95 | 96 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 97 | -------------------------------------------------------------------------------- /test/linters/params/eslint-typescript.js: -------------------------------------------------------------------------------- 1 | const ESLint = require("../../../src/linters/eslint"); 2 | const { joinDoubleBackslash } = require("../../test-utils"); 3 | 4 | const testName = "eslint-typescript"; 5 | const linter = ESLint; 6 | const args = ""; 7 | const commandPrefix = ""; 8 | const extensions = ["js", "ts"]; 9 | 10 | // Linting without auto-fixing 11 | function getLintParams(dir) { 12 | const stdoutFile1 = `{"filePath":"${joinDoubleBackslash( 13 | dir, 14 | "file1.ts", 15 | )}","messages":[{"ruleId":"prefer-const","severity":2,"message":"'str' is never reassigned. Use 'const' instead.","line":1,"column":5,"nodeType":"Identifier","messageId":"useConst","endLine":1,"endColumn":8,"fix":{"range":[0,18],"text":"const str = 'world';"}},{"ruleId":"no-warning-comments","severity":1,"message":"Unexpected 'todo' comment: 'TODO: Change something'.","line":5,"column":31,"nodeType":"Line","messageId":"unexpectedComment","endLine":5,"endColumn":56}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":1,"fixableWarningCount":0,"source":"let str = 'world'; // \\"prefer-const\\" warning\\n\\nfunction main(): void {\\n\\t// \\"no-warning-comments\\" error\\n\\tconsole.log('hello ' + str); // TODO: Change something\\n}\\n\\nmain();\\n","usedDeprecatedRules":[]}`; 16 | const stdoutFile2 = `{"filePath":"${joinDoubleBackslash( 17 | dir, 18 | "file2.js", 19 | )}","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'str' is assigned a value but never used.","line":1,"column":7,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":10}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const str = 'Hello world'; // \\"no-unused-vars\\" error\\n","usedDeprecatedRules":[]}`; 20 | return { 21 | // Expected output of the linting function 22 | cmdOutput: { 23 | status: 1, 24 | stdoutParts: [stdoutFile1, stdoutFile2], 25 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 26 | }, 27 | // Expected output of the parsing function 28 | lintResult: { 29 | isSuccess: false, 30 | warning: [ 31 | { 32 | path: "file1.ts", 33 | firstLine: 5, 34 | lastLine: 5, 35 | message: "Unexpected 'todo' comment: 'TODO: Change something' (no-warning-comments)", 36 | }, 37 | ], 38 | error: [ 39 | { 40 | path: "file1.ts", 41 | firstLine: 1, 42 | lastLine: 1, 43 | message: "'str' is never reassigned. Use 'const' instead (prefer-const)", 44 | }, 45 | { 46 | path: "file2.js", 47 | firstLine: 1, 48 | lastLine: 1, 49 | message: "'str' is assigned a value but never used (no-unused-vars)", 50 | }, 51 | ], 52 | }, 53 | }; 54 | } 55 | 56 | // Linting with auto-fixing 57 | function getFixParams(dir) { 58 | const stdoutFile1 = `{"filePath":"${joinDoubleBackslash( 59 | dir, 60 | "file1.ts", 61 | )}","messages":[{"ruleId":"no-warning-comments","severity":1,"message":"Unexpected 'todo' comment: 'TODO: Change something'.","line":5,"column":31,"nodeType":"Line","messageId":"unexpectedComment","endLine":5,"endColumn":56}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"output":"const str = 'world'; // \\"prefer-const\\" warning\\n\\nfunction main(): void {\\n\\t// \\"no-warning-comments\\" error\\n\\tconsole.log('hello ' + str); // TODO: Change something\\n}\\n\\nmain();\\n","usedDeprecatedRules":[]}`; 62 | const stdoutFile2 = `{"filePath":"${joinDoubleBackslash( 63 | dir, 64 | "file2.js", 65 | )}","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'str' is assigned a value but never used.","line":1,"column":7,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":10}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const str = 'Hello world'; // \\"no-unused-vars\\" error\\n","usedDeprecatedRules":[]}`; 66 | return { 67 | // Expected output of the linting function 68 | cmdOutput: { 69 | status: 1, 70 | stdoutParts: [stdoutFile1, stdoutFile2], 71 | stdout: `[${stdoutFile1},${stdoutFile2}]`, 72 | }, 73 | // Expected output of the parsing function 74 | lintResult: { 75 | isSuccess: false, 76 | warning: [ 77 | { 78 | path: "file1.ts", 79 | firstLine: 5, 80 | lastLine: 5, 81 | message: "Unexpected 'todo' comment: 'TODO: Change something' (no-warning-comments)", 82 | }, 83 | ], 84 | error: [ 85 | { 86 | path: "file2.js", 87 | firstLine: 1, 88 | lastLine: 1, 89 | message: "'str' is assigned a value but never used (no-unused-vars)", 90 | }, 91 | ], 92 | }, 93 | }; 94 | } 95 | 96 | module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; 97 | -------------------------------------------------------------------------------- /test/linters/linters.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const { copy, remove } = require("fs-extra"); 4 | 5 | const { normalizeDates, normalizePaths, createTmpDir } = require("../test-utils"); 6 | const autopep8Params = require("./params/autopep8"); 7 | const blackParams = require("./params/black"); 8 | const clangFormatParams = require("./params/clang-format"); 9 | const clippyParams = require("./params/clippy"); 10 | const dotnetFormatParams = require("./params/dotnet-format"); 11 | const erblintParams = require("./params/erblint"); 12 | const eslintParams = require("./params/eslint"); 13 | const eslintTypescriptParams = require("./params/eslint-typescript"); 14 | const flake8Params = require("./params/flake8"); 15 | const gofmtParams = require("./params/gofmt"); 16 | const golintParams = require("./params/golint"); 17 | const mypyParams = require("./params/mypy"); 18 | const phpCodeSnifferParams = require("./params/php-codesniffer"); 19 | const prettierParams = require("./params/prettier"); 20 | const pylintParams = require("./params/pylint"); 21 | const ruboCopParams = require("./params/rubocop"); 22 | const rustfmtParams = require("./params/rustfmt"); 23 | const stylelintParams = require("./params/stylelint"); 24 | const swiftFormatLockwood = require("./params/swift-format-lockwood"); 25 | // const swiftFormatOfficial = require("./params/swift-format-official"); 26 | const swiftlintParams = require("./params/swiftlint"); 27 | const tscParams = require("./params/tsc"); 28 | const xoParams = require("./params/xo"); 29 | 30 | const linterParams = [ 31 | autopep8Params, 32 | blackParams, 33 | clangFormatParams, 34 | clippyParams, 35 | dotnetFormatParams, 36 | erblintParams, 37 | eslintParams, 38 | eslintTypescriptParams, 39 | flake8Params, 40 | gofmtParams, 41 | golintParams, 42 | mypyParams, 43 | phpCodeSnifferParams, 44 | prettierParams, 45 | pylintParams, 46 | ruboCopParams, 47 | rustfmtParams, 48 | stylelintParams, 49 | tscParams, 50 | xoParams, 51 | ]; 52 | if (process.platform === "linux") { 53 | // Temporarily disabled because swift-format 0.50300.0 no longer returns a proper exit code, yet 54 | // returns the errors in STDERR. 55 | // linterParams.push(swiftFormatOfficial); 56 | } 57 | if (process.platform === "darwin") { 58 | linterParams.push(swiftFormatLockwood, swiftlintParams); 59 | } 60 | 61 | const tmpDir = createTmpDir(); 62 | jest.setTimeout(300000); 63 | 64 | // Copy linter test projects into temporary directory 65 | beforeAll(async () => { 66 | await copy(join(__dirname, "projects"), tmpDir); 67 | }); 68 | 69 | afterAll(async () => { 70 | await remove(tmpDir); 71 | }); 72 | 73 | // Test all linters 74 | describe.each(linterParams)( 75 | "%s", 76 | (projectName, linter, commandPrefix, extensions, args, getLintParams, getFixParams) => { 77 | const projectTmpDir = join(tmpDir, projectName); 78 | beforeAll(async () => { 79 | await expect(linter.verifySetup(projectTmpDir, commandPrefix)).resolves.toEqual(undefined); 80 | }); 81 | 82 | // Test lint and auto-fix modes 83 | describe.each([ 84 | ["lint", false], 85 | ["auto-fix", true], 86 | ])("%s", (lintMode, autoFix) => { 87 | const expected = autoFix ? getFixParams(projectTmpDir) : getLintParams(projectTmpDir); 88 | 89 | // Test `lint` function 90 | test(`${linter.name} returns expected ${lintMode} output`, () => { 91 | const cmdOutput = linter.lint(projectTmpDir, extensions, args, autoFix, commandPrefix); 92 | 93 | // Exit code 94 | expect(cmdOutput.status).toEqual(expected.cmdOutput.status); 95 | 96 | // stdout 97 | let stdout = normalizeDates(cmdOutput.stdout); 98 | stdout = normalizePaths(stdout, tmpDir); 99 | if ("stdoutParts" in expected.cmdOutput) { 100 | expected.cmdOutput.stdoutParts.forEach((stdoutPart) => 101 | expect(stdout).toEqual(expect.stringContaining(stdoutPart)), 102 | ); 103 | } else if ("stdout" in expected.cmdOutput) { 104 | expect(stdout).toEqual(expected.cmdOutput.stdout); 105 | } 106 | 107 | // stderr 108 | let stderr = normalizeDates(cmdOutput.stderr); 109 | stderr = normalizePaths(stderr, tmpDir); 110 | if ("stderrParts" in expected.cmdOutput) { 111 | expected.cmdOutput.stderrParts.forEach((stderrParts) => 112 | expect(stderr).toEqual(expect.stringContaining(stderrParts)), 113 | ); 114 | } else if ("stderr" in expected.cmdOutput) { 115 | expect(stderr).toEqual(expected.cmdOutput.stderr); 116 | } 117 | }); 118 | 119 | // Test `parseOutput` function 120 | test(`${linter.name} parses ${lintMode} output correctly`, () => { 121 | const lintResult = linter.parseOutput(projectTmpDir, expected.cmdOutput); 122 | expect(lintResult).toEqual(expected.lintResult); 123 | }); 124 | }); 125 | }, 126 | ); 127 | -------------------------------------------------------------------------------- /src/github/context.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | 3 | const core = require("@actions/core"); 4 | 5 | const { name: actionName } = require("../../package.json"); 6 | const { getEnv } = require("../utils/action"); 7 | 8 | /** 9 | * GitHub Actions workflow's environment variables 10 | * @typedef ActionEnv 11 | * @property {string} actor Event actor. 12 | * @property {string} eventName Event name. 13 | * @property {string} eventPath Event path. 14 | * @property {string} token Token. 15 | * @property {string} workspace Workspace path. 16 | */ 17 | 18 | /** 19 | * Information about the GitHub repository and its fork (if it exists) 20 | * @typedef GithubRepository 21 | * @property {string} repoName Repo name. 22 | * @property {string} cloneUrl Repo clone URL. 23 | * @property {string} forkName Fork name. 24 | * @property {string} forkCloneUrl Fork repo clone URL. 25 | * @property {boolean} hasFork Whether repo has a fork. 26 | */ 27 | 28 | /** 29 | * Information about the GitHub repository and action trigger event 30 | * @typedef GithubContext 31 | * @property {string} actor Event actor. 32 | * @property {string} branch Branch name. 33 | * @property {object} event Event. 34 | * @property {string} eventName Event name. 35 | * @property {GithubRepository} repository Information about the GitHub repository 36 | * @property {string} token Token. 37 | * @property {string} workspace Workspace path. 38 | */ 39 | 40 | /** 41 | * Returns the GitHub Actions workflow's environment variables 42 | * @returns {ActionEnv} GitHub Actions workflow's environment variables 43 | */ 44 | function parseActionEnv() { 45 | return { 46 | // Information provided by environment 47 | actor: getEnv("github_actor", true), 48 | eventName: getEnv("github_event_name", true), 49 | eventPath: getEnv("github_event_path", true), 50 | workspace: getEnv("github_workspace", true), 51 | 52 | // Information provided by action user 53 | token: core.getInput("github_token", { required: true }), 54 | }; 55 | } 56 | 57 | /** 58 | * Parse `event.json` file (file with the complete webhook event payload, automatically provided by 59 | * GitHub) 60 | * @param {string} eventPath - Path to the `event.json` file 61 | * @returns {object} - Webhook event payload 62 | */ 63 | function parseEnvFile(eventPath) { 64 | const eventBuffer = readFileSync(eventPath); 65 | return JSON.parse(eventBuffer); 66 | } 67 | 68 | /** 69 | * Parses the name of the current branch from the GitHub webhook event 70 | * @param {string} eventName - GitHub event type 71 | * @param {object} event - GitHub webhook event payload 72 | * @returns {string} - Branch name 73 | */ 74 | function parseBranch(eventName, event) { 75 | if (eventName === "push" || eventName === "workflow_dispatch") { 76 | return event.ref.substring(11); // Remove "refs/heads/" from start of string 77 | } 78 | if (eventName === "pull_request" || eventName === "pull_request_target") { 79 | return event.pull_request.head.ref; 80 | } 81 | throw Error(`${actionName} does not support "${eventName}" GitHub events`); 82 | } 83 | 84 | /** 85 | * Parses the name of the current repository and determines whether it has a corresponding fork. 86 | * Fork detection is only supported for the "pull_request" event 87 | * @param {string} eventName - GitHub event type 88 | * @param {object} event - GitHub webhook event payload 89 | * @returns {GithubRepository} - Information about the GitHub repository and its fork (if it exists) 90 | */ 91 | function parseRepository(eventName, event) { 92 | const repoName = event.repository.full_name; 93 | const cloneUrl = event.repository.clone_url; 94 | let forkName; 95 | let forkCloneUrl; 96 | if (eventName === "pull_request" || eventName === "pull_request_target") { 97 | // "pull_request" events are triggered on the repository where the PR is made. The PR branch can 98 | // be on the same repository (`forkRepository` is set to `null`) or on a fork (`forkRepository` 99 | // is defined) 100 | const headRepoName = event.pull_request.head.repo.full_name; 101 | forkName = repoName === headRepoName ? undefined : headRepoName; 102 | const headForkCloneUrl = event.pull_request.head.repo.clone_url; 103 | forkCloneUrl = cloneUrl === headForkCloneUrl ? undefined : headForkCloneUrl; 104 | } 105 | return { 106 | repoName, 107 | cloneUrl, 108 | forkName, 109 | forkCloneUrl, 110 | hasFork: forkName != null && forkName !== repoName, 111 | }; 112 | } 113 | 114 | /** 115 | * Returns information about the GitHub repository and action trigger event 116 | * @returns {GithubContext} context - Information about the GitHub repository and action trigger 117 | * event 118 | */ 119 | function getContext() { 120 | const { actor, eventName, eventPath, token, workspace } = parseActionEnv(); 121 | const event = parseEnvFile(eventPath); 122 | return { 123 | actor, 124 | branch: parseBranch(eventName, event), 125 | event, 126 | eventName, 127 | repository: parseRepository(eventName, event), 128 | token, 129 | workspace, 130 | }; 131 | } 132 | 133 | module.exports = { 134 | getContext, 135 | parseActionEnv, 136 | parseBranch, 137 | parseEnvFile, 138 | parseRepository, 139 | }; 140 | --------------------------------------------------------------------------------