Skip to content

Commit

Permalink
chore: add lint workflows, configs and scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
rebornplusplus committed Feb 23, 2024
1 parent a5cf4a0 commit 2678575
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
177 changes: 177 additions & 0 deletions .github/scripts/lint/lint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/python3

"""
Custom linter for chisel slice definition files.
Use in addition with yamllint (https://yamllint.readthedocs.io), since this
script only covers slice definition file specifc rules.
"""

import argparse
import sys
import typing
from dataclasses import dataclass

import yaml


def parse_args() -> argparse.Namespace:
"""
Parse CLI args passed to this script.
"""
parser = argparse.ArgumentParser(
description="Lint slice definition files",
)
parser.add_argument(
"files",
metavar="file",
help="Chisel slice definition file(s)",
nargs="*",
)
parser.add_argument(
"--sorted-slices",
action=argparse.BooleanOptionalAction,
help="Slice names must be sorted",
)
parser.add_argument(
"--sorted-essential",
action=argparse.BooleanOptionalAction,
help="Entries in 'essential' must be sorted",
)
parser.add_argument(
"--sorted-contents",
action=argparse.BooleanOptionalAction,
help="Entries in 'contents' need to be sorted",
)
return parser.parse_args()


def is_sorted(entries: list) -> bool:
"""
Return true if a list is sorted in ASCENDING order.
"""
for i in range(len(entries) - 1):
if entries[i] > entries[i + 1]:
return False
return True


def lint_sorted_slices(yaml_data: dict) -> list[str] | None:
"""
Slice names must be sorted.
"""
slices = list(yaml_data["slices"].keys())
if not is_sorted(slices):
return ["slice names are not sorted (--sorted-slices)"]
return None


def lint_sorted_essential(yaml_data: dict) -> list[str] | None:
"""
'essential' entries must be sorted in a slice.
"""
slices = yaml_data["slices"]
errs = []
for key, slice in slices.items():
if "essential" not in slice:
continue
entries = slice["essential"]
if is_sorted(entries):
continue
errs.append(
f'{key}: "essential" entries are not sorted (--sorted-essential)',
)
if len(errs) > 0:
return errs
return None


def lint_sorted_contents(yaml_data: dict) -> list[str] | None:
"""
'contents' entries must be sorted in a slice.
"""
slices = yaml_data["slices"]
errs = []
for key, slice in slices.items():
if "contents" not in slice:
continue
entries = list(slice["contents"].keys())
if is_sorted(entries):
continue
errs.append(
f'{key}: "contents" entries are not sorted (--sorted-contents)',
)
if len(errs) > 0:
return errs
return None


@dataclass
class LintOptions:
sorted_slices: bool = True
sorted_essential: bool = True
sorted_contents: bool = True


def lint(filename: str, opts: LintOptions) -> list[str] | None:
"""
Run all lint rules on a file using the provided options.
"""
with open(filename, "r", encoding="utf-8") as f:
data = f.read()
yaml_data = yaml.safe_load(data)

all_errs = []

def lint_yaml_data(func: typing.Callable):
errs = func(yaml_data)
if errs:
all_errs.extend(errs)

if opts.sorted_slices is not False:
lint_yaml_data(lint_sorted_slices)
if opts.sorted_essential is not False:
lint_yaml_data(lint_sorted_essential)
if opts.sorted_contents is not False:
lint_yaml_data(lint_sorted_contents)

if len(all_errs) > 0:
return all_errs
return None


def print_errors(errs: dict[str, list[str]] | None) -> None:
"""
Print the found linting errors.
"""
if not errs:
return
for filename in sorted(errs.keys()):
print(f"\033[4m{filename}\033[0m")
for e in sorted(errs[filename]):
print(f" \033[91m{'error':8s}\033[0m{e}")
print()


def main() -> None:
"""
The main function -- execution should start from here.
"""
args = parse_args()
files = args.files
opts = LintOptions(args.sorted_slices, args.sorted_essential, args.sorted_contents)
#
ok = True
errs = {}
for file in files:
e = lint(file, opts)
if e:
errs[file] = e
ok = False
print_errors(errs)
if not ok:
sys.exit(1)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions .github/scripts/lint/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyyaml
98 changes: 98 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Lint

on:
push:
branches:
- "main"
paths:
- ".github/**"
pull_request:
branches:
- "main"
paths:
- ".github/**"
schedule:
# Run at 00:00 every day.
# Ref: https://man7.org/linux/man-pages/man5/crontab.5.html
- cron: "0 0 * * *"
workflow_call:

env:
# chisel-releases branches to lint on.
RELEASES: ${{ toJson('["ubuntu-20.04","ubuntu-22.04","ubuntu-23.10","ubuntu-24.04"]') }}

jobs:
prepare-lint:
runs-on: ubuntu-latest
name: "Prepare to lint"
outputs:
matrix: ${{ steps.set-output.outputs.matrix }}
steps:
- name: Set output
id: set-output
run: |
set -ex
if [[
"${{ github.base_ref || github.ref_name }}" == "main" ||
"${{ github.event_name }}" == "schedule"
]]; then
echo "matrix={\"ref\":${{ env.RELEASES }}}" >> $GITHUB_OUTPUT
else
echo "matrix={\"ref\":[\"\"]}" >> $GITHUB_OUTPUT
fi
lint:
runs-on: ubuntu-latest
name: "Lint"
needs: prepare-lint
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare-lint.outputs.matrix) }}
env:
main-branch-path: files-from-main
steps:
- uses: actions/checkout@v4
with:
ref: ${{ matrix.ref }}

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
path: ${{ env.main-branch-path }}

- name: Install dependencies
env:
script-dir: "${{ env.main-branch-path }}/.github/scripts/lint"
run: |
set -ex
pip install --upgrade pip
pip install yamllint
pip install -r "${{ env.script-dir }}/requirements.txt"
ln -s "${{ env.script-dir }}/lint.py" lint
- name: Lint with yamllint
env:
config-path: "${{ env.main-branch-path }}/.github/yamllint.yaml"
run: |
set -ex
# We need to enable globstar to use the ** patterns below.
shopt -s globstar
yamllint -c "${{ env.config-path }}" \
chisel.yaml \
slices/
- name: Lint with SDF-specific custom linter
run: |
set -ex
# We need to enable globstar to use the ** patterns below.
shopt -s globstar
./lint slices/**/*.yaml
33 changes: 33 additions & 0 deletions .github/yamllint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# yamllint configurations.
# Ref: https://yamllint.readthedocs.io/en/stable/configuration.html

extends: default

# Ref: https://yamllint.readthedocs.io/en/stable/rules.html
rules:
braces:
forbid: false
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
brackets:
forbid: false
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
comments:
level: error
comments-indentation:
level: error
document-end:
present: false
document-start:
present: false
empty-lines:
max: 1
indentation:
spaces: 2
line-length:
max: 80

0 comments on commit 2678575

Please sign in to comment.