Skip to content

Commit d7ebe91

Browse files
authored
Merge pull request #7 from advanced-security/pyre
Support Pyre type checker
2 parents 4d4691b + 253f3b9 commit d7ebe91

File tree

6 files changed

+145
-18
lines changed

6 files changed

+145
-18
lines changed

.github/workflows/lint.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@ on:
88
schedule:
99
- cron: '22 3 * * 2'
1010
workflow_dispatch:
11+
1112
jobs:
1213
lint:
13-
runs-on: ubuntu-latest
14+
runs-on: ${{ matrix.os }}
1415
strategy:
1516
matrix:
16-
linter: ['flake8', 'pylint', 'ruff', 'mypy', 'pytype', 'pyright', 'fixit']
17-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
17+
linter: ['flake8', 'pylint', 'ruff', 'mypy', 'pytype', 'pyright', 'fixit', 'pyre']
18+
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
19+
os: [ubuntu-latest, macos-latest] # doesn't yet work on Windows
1820
fail-fast: false
21+
1922
steps:
2023
# install dependencies for all linters, then run the linter, so we don't get import failures when the linters scan the code
2124
# upgrade pip, so that we can install flake8_sarif_formatter properly from the git repo
2225
- uses: actions/checkout@v4
2326
- name: Install pip dependencies
2427
run: |
2528
python3 -mpip install -q --upgrade pip
26-
python3 -mpip install -q flake8 pylint ruff mypy pytype pyright fixit
29+
python3 -mpip install -q flake8 pylint ruff mypy pytype pyright fixit pyre-check
2730
python3 -mpip install -q flake8-sarif-formatter
2831
- name: Run Python Lint
2932
uses: advanced-security/python-lint-code-scanning-action@main

.github/workflows/lint_win.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Python Lint Workflow
2+
on:
3+
workflow_dispatch:
4+
5+
jobs:
6+
lint:
7+
runs-on: ${{ matrix.os }}
8+
strategy:
9+
matrix:
10+
linter: ['flake8', 'pylint', 'ruff', 'mypy', 'pytype', 'pyright', 'fixit', 'pyre']
11+
python-version: ['3.10']
12+
os: [windows-latest] # this will fail on Windows currently
13+
fail-fast: false
14+
15+
steps:
16+
# install dependencies for all linters, then run the linter, so we don't get import failures when the linters scan the code
17+
# upgrade pip, so that we can install flake8_sarif_formatter properly from the git repo
18+
- uses: actions/checkout@v4
19+
- name: Install pip dependencies
20+
run: |
21+
python3 -mpip install -q --upgrade pip
22+
python3 -mpip install -q flake8 pylint ruff mypy pytype pyright fixit pyre-check
23+
python3 -mpip install -q flake8-sarif-formatter
24+
- name: Run Python Lint
25+
uses: advanced-security/python-lint-code-scanning-action@main
26+
with:
27+
linter: ${{ matrix.linter }}
28+
python-version: ${{ matrix.python-version }}

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ build
22
*.egg-info
33
.ruff_cache
44
*.sarif
5-
*.pyc
5+
*.pyc
6+
.pyre
7+
.pytype
8+
.mypy_cache

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# CHANGELOG
22

3+
## 1.0.2 - 2023-10-16
4+
5+
* Added Pyre
6+
* Typecheckers now use a current clone of `typeshed` vs their shipped version
7+
8+
## 1.0.1 - 2023-10-10
9+
10+
* Improved error handling
11+
* Pinning for linter versions
12+
* Using PyPi location of `flake8-sarif-formatter`
13+
* Quieter log output
14+
315
## 1.0.0 - 2023-10-06
416

517
* Initial open source release

action.yml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ inputs:
88
description: 'The linter to use'
99
required: true
1010
default: 'flake8'
11-
choices: ['ruff', 'flake8', 'pylint', 'mypy', 'pyright', 'pytype', 'fixit']
11+
choices: ['ruff', 'flake8', 'pylint', 'mypy', 'pyright', 'pytype', 'fixit', 'pyre']
1212
target:
1313
description: 'The target to lint'
1414
required: true
@@ -50,6 +50,14 @@ inputs:
5050
description: 'The version of fixit to use'
5151
required: false
5252
default: 'latest'
53+
pyre-version:
54+
description: 'The version of pyre to use'
55+
required: false
56+
default: 'latest'
57+
typeshed-version:
58+
description: 'The version of typeshed to use'
59+
required: false
60+
default: 'main'
5361
runs:
5462
using: 'composite'
5563
steps:
@@ -73,8 +81,9 @@ runs:
7381
"${PYTHON_CMD}" -mpip install --upgrade pip
7482
7583
# set up linter variables
76-
linters=('ruff' 'flake8' 'pylint' 'mypy' 'pyright' 'pytype' 'fixit')
84+
linters=('ruff' 'flake8' 'pylint' 'mypy' 'pyright' 'pytype' 'fixit' 'pyre')
7785
install_flake8_formatter_linters=('ruff', 'flake8')
86+
install_typeshed_linters=('pyre', 'pytype', 'mypy', 'pyright')
7887
EXTRA_PIP_FLAGS=''
7988
LINTER_VERSION_CONSTRAINT=''
8089
EXTRA_LINTER_SCRIPT_FLAGS=''
@@ -115,14 +124,24 @@ runs:
115124
if [[ "${INPUTS_PYTYPE_VERSION}" != "latest" ]]; then
116125
LINTER_VERSION_CONSTRAINT="==${INPUTS_PYTYPE_VERSION}"
117126
fi
127+
elif [[ "${INPUTS_LINTER}" == "pyre" ]]; then
128+
if [[ "${INPUTS_PYRE_VERSION}" != "latest" ]]; then
129+
LINTER_VERSION_CONSTRAINT="==${INPUTS_PYRE_VERSION}"
130+
fi
118131
fi
119132
fi
120133
121134
echo "::debug::Installing ${INPUTS_LINTER}${LINTER_VERSION_CONSTRAINT} for Python ${INPUTS_PYTHON_VERSION}"
122135
123136
# install linter
124-
if ! "${PYTHON_CMD}" -mpip install -q "${INPUTS_LINTER}${LINTER_VERSION_CONSTRAINT}" ${EXTRA_PIP_FLAGS}; then
125-
echo "::error::${INPUTS_LINTER}${LINTER_VERSION_CONSTRAINT} failed to install for Python ${INPUTS_PYTHON_VERSION}"
137+
LINTER_PACKAGE="${INPUTS_LINTER}"
138+
139+
if [[ "${INPUTS_LINTER}" == "pyre" ]]; then
140+
LINTER_PACKAGE="pyre-check"
141+
fi
142+
143+
if ! "${PYTHON_CMD}" -mpip install -q "${LINTER_PACKAGE}${LINTER_VERSION_CONSTRAINT}" ${EXTRA_PIP_FLAGS}; then
144+
echo "::error::${LINTER_PACKAGE}${LINTER_VERSION_CONSTRAINT} failed to install for Python ${INPUTS_PYTHON_VERSION}"
126145
# if it is fixit on 3.7, just exit 0, we know it's not available
127146
if [[ "${INPUTS_LINTER}" == "fixit" && "${INPUTS_PYTHON_VERSION}" == "3.7" ]]; then
128147
exit 0
@@ -141,6 +160,14 @@ runs:
141160
EXTRA_LINTER_SCRIPT_FLAGS=" --debug"
142161
fi
143162
163+
# install typeshed if needed (for typecheckers)
164+
if [[ "${install_typeshed_linters[*]}" =~ (^|[^[:alpha:]])${INPUTS_LINTER}([^[:alpha:]]|$) ]]; then
165+
echo "::debug::Installing typeshed for ${INPUTS_LINTER}"
166+
# clone from GitHub
167+
gh repo clone python/typeshed -- --depth 1 --branch "${INPUTS_TYPESHED_VERSION}" "${GITHUB_WORKSPACE}/typeshed" || ( echo "::error::typeshed failed to install for Python ${INPUTS_PYTHON_VERSION}" && exit 1 )
168+
EXTRA_LINTER_SCRIPT_FLAGS+=" --typeshed-path=${GITHUB_WORKSPACE}/typeshed"
169+
fi
170+
144171
# run linter
145172
if ! "${PYTHON_CMD}" "${GITHUB_ACTION_PATH}"/python_lint.py "${INPUTS_LINTER}" --target="${INPUTS_TARGET}" --output="${GITHUB_WORKSPACE}/${INPUTS_OUTPUT}" ${EXTRA_LINTER_SCRIPT_FLAGS}; then
146173
# don't fail "hard" if it's known failures that we cannot account for (yet)
@@ -168,6 +195,8 @@ runs:
168195
INPUTS_PYRIGHT_VERSION: ${{ inputs.pyright-version }}
169196
INPUTS_PYTYPE_VERSION: ${{ inputs.pytype-version }}
170197
INPUTS_FIXIT_VERSION: ${{ inputs.fixit-version }}
198+
INPUTS_PYRE_VERSION: ${{ inputs.pyre-version }}
199+
INPUTS_TYPESHED_VERSION: ${{ inputs.typeshed-version }}
171200
shell: bash
172201
- name: Upload SARIF
173202
if: ${{ hashFiles(inputs.output) != '' }}

python_lint.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import sys
14+
import os
1415
import logging
1516
from argparse import ArgumentParser
1617
from pathlib import Path
@@ -54,7 +55,7 @@ def make_sarif_run(tool_name: str) -> dict:
5455
return sarif_run
5556

5657

57-
def flake8_linter(target: Path) -> None:
58+
def flake8_linter(target: Path, *args) -> None:
5859
"""Run the flake8 linter.
5960
6061
In contrast to the other linters, flake8 has plugin architecture.
@@ -154,7 +155,7 @@ def ruff_format_sarif(results: List[Dict[str, Any]], target: Path) -> dict:
154155
return sarif_run
155156

156157

157-
def ruff_linter(target: Path) -> Optional[dict]:
158+
def ruff_linter(target: Path, *args) -> Optional[dict]:
158159
"""Run the ruff linter."""
159160
try:
160161
# pylint: disable=import-outside-toplevel
@@ -256,7 +257,7 @@ def pylint_format_sarif(results: List[Dict[str, Any]], target: Path) -> dict:
256257
return sarif_run
257258

258259

259-
def pylint_linter(target: Path) -> Optional[dict]:
260+
def pylint_linter(target: Path, *args) -> Optional[dict]:
260261
"""Run the pylint linter."""
261262
process = run(
262263
["pylint", "--output-format=json", "--recursive=y", target.absolute().as_posix()],
@@ -371,7 +372,7 @@ def mypy_format_sarif(mypy_results: str, target: Path) -> dict:
371372
return sarif_run
372373

373374

374-
def mypy_linter(target: Path) -> Optional[dict]:
375+
def mypy_linter(target: Path, typeshed_path: Path) -> Optional[dict]:
375376
"""Run the mypy linter."""
376377
mypy_args = [
377378
"--install-types",
@@ -383,6 +384,8 @@ def mypy_linter(target: Path) -> Optional[dict]:
383384
"--show-column-numbers",
384385
"--show-error-end",
385386
"--show-absolute-path",
387+
"--custom-typeshed-dir",
388+
typeshed_path.as_posix(),
386389
]
387390

388391
process_lint = run(["mypy", *mypy_args, target.absolute().as_posix()], capture_output=True, check=False)
@@ -462,9 +465,13 @@ def pyright_format_sarif(results: dict, target: Path) -> dict:
462465
return sarif_run
463466

464467

465-
def pyright_linter(target: Path) -> Optional[dict]:
468+
def pyright_linter(target: Path, typeshed_path: Path) -> Optional[dict]:
466469
"""Run the pyright linter."""
467-
process = run(["pyright", "--outputjson", target.absolute().as_posix()], capture_output=True, check=False)
470+
process = run(
471+
["pyright", "--outputjson", "--typeshedpath", typeshed_path, target.absolute().as_posix()],
472+
capture_output=True,
473+
check=False,
474+
)
468475

469476
if process.stderr:
470477
LOG.error("STDERR: %s", process.stderr.decode("utf-8"))
@@ -545,10 +552,14 @@ def pytype_format_sarif(results: str, target: Path) -> dict:
545552
return sarif_run
546553

547554

548-
def pytype_linter(target: Path) -> Optional[dict]:
555+
def pytype_linter(target: Path, typeshed_path: Path) -> Optional[dict]:
549556
"""Run the pytype linter."""
557+
os.environ["TYPESHED_HOME"] = typeshed_path.as_posix()
558+
550559
process = run(
551-
["pytype", "--exclude", ".pytype/", "--", target.absolute().as_posix()], capture_output=True, check=False
560+
["pytype", "--exclude", ".pytype/", "--", target.as_posix()],
561+
capture_output=True,
562+
check=False,
552563
)
553564

554565
if process.stderr:
@@ -567,6 +578,44 @@ def pytype_linter(target: Path) -> Optional[dict]:
567578
return sarif_run
568579

569580

581+
def pyre_linter(target: Path, typeshed_path: Path) -> Optional[dict]:
582+
"""Run the pytype linter."""
583+
process = run(
584+
[
585+
"pyre",
586+
"--source-directory",
587+
target.as_posix(),
588+
"--output",
589+
"sarif",
590+
"--typeshed",
591+
typeshed_path.as_posix(),
592+
"check",
593+
],
594+
capture_output=True,
595+
check=False,
596+
)
597+
598+
if process.stderr:
599+
LOG.debug("STDERR: %s", process.stderr.decode("utf-8"))
600+
601+
if not process.stdout:
602+
LOG.error("No output from pytype")
603+
return None
604+
605+
try:
606+
sarif = json.loads(process.stdout.decode("utf-8"))
607+
except json.JSONDecodeError as err:
608+
LOG.error("Unable to parse pyre output: %s", err)
609+
LOG.debug("Output: %s", process.stdout.decode("utf-8"))
610+
return None
611+
612+
if "runs" in sarif and len(sarif["runs"]) > 0:
613+
return sarif["runs"][0]
614+
615+
LOG.error("SARIF not correctly formed, or no runs to output")
616+
return None
617+
618+
570619
def make_fixit_description(rule: str) -> str:
571620
"""Format 'SomeRuleDescription' into 'Some rule description'."""
572621
rule = FIND_CAMEL_CASE.sub(lambda x: x.group(0).lower() + " ", rule)
@@ -670,6 +719,7 @@ def make_paths_relative_to_target(runs: List[dict], target: Path) -> None:
670719
"mypy": mypy_linter,
671720
"pyright": pyright_linter,
672721
"fixit": fixit_linter,
722+
"pyre": pyre_linter,
673723
}
674724

675725
# pytype is only supported on Python 3.10 and below, at the time of writing
@@ -682,6 +732,7 @@ def add_args(parser: ArgumentParser) -> None:
682732
parser.add_argument("linter", choices=LINTERS.keys(), nargs="+", help="The linter(s) to use")
683733
parser.add_argument("--target", "-t", default=".", required=False, help="Target path for the linter")
684734
parser.add_argument("--output", "-o", default="python_linter.sarif", required=False, help="Output filename")
735+
parser.add_argument("--typeshed-path", required=False, help="Path to typeshed")
685736
parser.add_argument("--debug", "-d", action="store_true", required=False, help="Enable debug logging")
686737

687738

@@ -700,11 +751,12 @@ def main() -> None:
700751
sarif_runs: List[dict] = []
701752

702753
target = Path(args.target).resolve().absolute()
754+
typeshed_path = Path(args.typeshed_path).resolve().absolute()
703755

704756
for linter in args.linter:
705757
LOG.debug("Running %s", linter)
706758

707-
sarif_run = LINTERS[linter](target)
759+
sarif_run = LINTERS[linter](target, typeshed_path)
708760

709761
if sarif_run is not None:
710762
sarif_runs.append(sarif_run)

0 commit comments

Comments
 (0)