From 26785753947962e785eb88310ebe685c519d0fbc Mon Sep 17 00:00:00 2001 From: Rafid Bin Mostofa Date: Fri, 23 Feb 2024 21:10:15 +0600 Subject: [PATCH] chore: add lint workflows, configs and scripts --- .github/scripts/lint/lint.py | 177 ++++++++++++++++++++++++++ .github/scripts/lint/requirements.txt | 1 + .github/workflows/lint.yaml | 98 ++++++++++++++ .github/yamllint.yaml | 33 +++++ 4 files changed, 309 insertions(+) create mode 100755 .github/scripts/lint/lint.py create mode 100644 .github/scripts/lint/requirements.txt create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/yamllint.yaml diff --git a/.github/scripts/lint/lint.py b/.github/scripts/lint/lint.py new file mode 100755 index 000000000..5ab6acddc --- /dev/null +++ b/.github/scripts/lint/lint.py @@ -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() diff --git a/.github/scripts/lint/requirements.txt b/.github/scripts/lint/requirements.txt new file mode 100644 index 000000000..c3726e8bf --- /dev/null +++ b/.github/scripts/lint/requirements.txt @@ -0,0 +1 @@ +pyyaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..ba0670e03 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -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 diff --git a/.github/yamllint.yaml b/.github/yamllint.yaml new file mode 100644 index 000000000..0a0811f11 --- /dev/null +++ b/.github/yamllint.yaml @@ -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