Skip to content

Commit 647e435

Browse files
committed
pre-commit: validate hooks and auto create docs with Python
1 parent 867c93d commit 647e435

6 files changed

Lines changed: 332 additions & 0 deletions

File tree

.pre-commit-config.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@ repos:
1919
description: check hooks apply to the repository
2020
- repo: local
2121
hooks:
22+
- id: pre-commit-hook-validator
23+
name: run pre-commit hook validator
24+
description: Validates a pre-commit-config.yaml file to ensure all hooks have 'name' and 'description' keys
25+
entry: scripts/pre_commit_hooks_validator.py
26+
language: python
27+
additional_dependencies:
28+
- pyyaml
29+
files: ^\.pre-commit-config\.yaml$ # Only run this hook against the config file itself
30+
pass_filenames: false # Crucial: Don't pass the filename as an argument. Your script will default.
31+
# Alternatively, if you want to pass the filename, you'd need:
32+
# entry: python validate_pre_commit_hooks.py .pre-commit-config.yaml
33+
# args: ['.pre-commit-config.yaml']
34+
# pass_filenames: false
35+
require_serial: true
36+
- id: create-pre-commit-docs
37+
name: create pre-commit docs
38+
description: creates a Markdown file with information on the pre-commit hooks
39+
entry: scripts/create_pre_commit_docs.py
40+
language: python
41+
additional_dependencies:
42+
- pyyaml
43+
files: ^\.pre-commit-config\.yaml$
44+
pass_filenames: false
45+
require_serial: true
2246
- id: prettier
2347
name: run prettier
2448
description: format files with prettier
@@ -43,29 +67,67 @@ repos:
4367
rev: v5.0.0
4468
hooks:
4569
- id: check-added-large-files
70+
name: check added large files
71+
description: Prevent giant files from being committed
4672
- id: check-case-conflict
73+
name: check case conflict
74+
description: Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT
4775
- id: check-executables-have-shebangs
76+
name: check executables have shebangs
77+
description: Checks that non-binary executables have a proper shebang
4878
exclude: ^test/t/lang\.rb$
4979
- id: check-illegal-windows-names
80+
name: check illegal windows names
81+
description: Check for files that cannot be created on Windows
5082
- id: pretty-format-json
83+
name: pretty format json
84+
description: Checks that all your JSON files are pretty
5185
args: [--autofix, --no-sort-keys]
5286
- id: check-json
87+
name: check json
88+
description: Attempts to load all json files to verify syntax
5389
- id: check-merge-conflict
90+
name: check merge conflict
91+
description: Check for files that contain merge conflict strings
5492
- id: check-shebang-scripts-are-executable
93+
name: check shebang scripts are executable
94+
description: Checks that scripts with shebangs are executable
5595
- id: check-vcs-permalinks
96+
name: check vcs permalinks
97+
description: Ensures that links to vcs websites are permalinks
5698
- id: check-yaml
99+
name: check yaml
100+
description: Attempts to load all yaml files to verify syntax
57101
- id: destroyed-symlinks
102+
name: destroyed symlinks
103+
description: Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to
58104
- id: detect-aws-credentials
105+
name: detect aws credentials
106+
description: Checks for the existence of AWS secrets that you have set up with the AWS CLI
59107
args: [--allow-missing-credentials]
60108
- id: detect-private-key
109+
name: detect private key
110+
description: Checks for the existence of private keys
61111
- id: end-of-file-fixer
112+
name: end of file fixer
113+
description: Makes sure files end in a newline and only a newline
62114
- id: file-contents-sorter
115+
name: file contents sorter
116+
description: Sort the lines in specified files (defaults to alphabetical)
63117
args: [--unique]
64118
files: ^\.github/linters/codespell\.txt$
65119
- id: fix-byte-order-marker
120+
name: fix byte order marker
121+
description: Removes UTF-8 byte order marker
66122
- id: forbid-submodules
123+
name: forbid submodules
124+
description: Prevent addition of new git submodules
67125
- id: mixed-line-ending
126+
name: mixed line ending
127+
description: Replaces or checks mixed line ending
68128
- id: trailing-whitespace
129+
name: trailing whitespace
130+
description: Trims trailing whitespace
69131
- repo: https://github.com/Lucas-C/pre-commit-hooks
70132
rev: v1.5.5
71133
hooks:

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Ignore artifacts:
22
build
33
coverage
4+
doc/guides/pre-commit-hooks.md
45
doc/internal/opcode.md
56
tools/lrama

doc/guides/pre-commit-hooks.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# pre-commit hook documentation
2+
3+
| Hook ID | Language | Name | Description | Version |
4+
|---|---|---|---|---|
5+
| identity | N/A | run identity | check your identity | N/A |
6+
| check-hooks-apply | N/A | run check-hooks-apply | check hooks apply to the repository | N/A |
7+
| pre-commit-hook-validator | python | run pre-commit hook validator | Validates a pre-commit-config.yaml file to ensure all hooks have 'name' and 'description' keys | N/A |
8+
| create-pre-commit-docs | python | create pre-commit docs | creates a Markdown file with information on the pre-commit hooks | N/A |
9+
| prettier | node | run prettier | format files with prettier | N/A |
10+
| [gitleaks](https://github.com/gitleaks/gitleaks) | N/A | run gitleaks | detect hardcoded secrets with gitleaks | v8.27.2 |
11+
| [oxipng](https://github.com/shssoichiro/oxipng) | N/A | run oxipng | use lossless compression to optimize PNG files | v9.1.5 |
12+
| [check-added-large-files](https://github.com/pre-commit/pre-commit-hooks) | N/A | check added large files | Prevent giant files from being committed | v5.0.0 |
13+
| [check-case-conflict](https://github.com/pre-commit/pre-commit-hooks) | N/A | check case conflict | Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT | v5.0.0 |
14+
| [check-executables-have-shebangs](https://github.com/pre-commit/pre-commit-hooks) | N/A | check executables have shebangs | Checks that non-binary executables have a proper shebang | v5.0.0 |
15+
| [check-illegal-windows-names](https://github.com/pre-commit/pre-commit-hooks) | N/A | check illegal windows names | Check for files that cannot be created on Windows | v5.0.0 |
16+
| [pretty-format-json](https://github.com/pre-commit/pre-commit-hooks) | N/A | pretty format json | Checks that all your JSON files are pretty | v5.0.0 |
17+
| [check-json](https://github.com/pre-commit/pre-commit-hooks) | N/A | check json | Attempts to load all json files to verify syntax | v5.0.0 |
18+
| [check-merge-conflict](https://github.com/pre-commit/pre-commit-hooks) | N/A | check merge conflict | Check for files that contain merge conflict strings | v5.0.0 |
19+
| [check-shebang-scripts-are-executable](https://github.com/pre-commit/pre-commit-hooks) | N/A | check shebang scripts are executable | Checks that scripts with shebangs are executable | v5.0.0 |
20+
| [check-vcs-permalinks](https://github.com/pre-commit/pre-commit-hooks) | N/A | check vcs permalinks | Ensures that links to vcs websites are permalinks | v5.0.0 |
21+
| [check-yaml](https://github.com/pre-commit/pre-commit-hooks) | N/A | check yaml | Attempts to load all yaml files to verify syntax | v5.0.0 |
22+
| [destroyed-symlinks](https://github.com/pre-commit/pre-commit-hooks) | N/A | destroyed symlinks | Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to | v5.0.0 |
23+
| [detect-aws-credentials](https://github.com/pre-commit/pre-commit-hooks) | N/A | detect aws credentials | Checks for the existence of AWS secrets that you have set up with the AWS CLI | v5.0.0 |
24+
| [detect-private-key](https://github.com/pre-commit/pre-commit-hooks) | N/A | detect private key | Checks for the existence of private keys | v5.0.0 |
25+
| [end-of-file-fixer](https://github.com/pre-commit/pre-commit-hooks) | N/A | end of file fixer | Makes sure files end in a newline and only a newline | v5.0.0 |
26+
| [file-contents-sorter](https://github.com/pre-commit/pre-commit-hooks) | N/A | file contents sorter | Sort the lines in specified files (defaults to alphabetical) | v5.0.0 |
27+
| [fix-byte-order-marker](https://github.com/pre-commit/pre-commit-hooks) | N/A | fix byte order marker | Removes UTF-8 byte order marker | v5.0.0 |
28+
| [forbid-submodules](https://github.com/pre-commit/pre-commit-hooks) | N/A | forbid submodules | Prevent addition of new git submodules | v5.0.0 |
29+
| [mixed-line-ending](https://github.com/pre-commit/pre-commit-hooks) | N/A | mixed line ending | Replaces or checks mixed line ending | v5.0.0 |
30+
| [trailing-whitespace](https://github.com/pre-commit/pre-commit-hooks) | N/A | trailing whitespace | Trims trailing whitespace | v5.0.0 |
31+
| [forbid-tabs](https://github.com/Lucas-C/pre-commit-hooks) | N/A | run no-tabs checker | check the codebase for tabs | v1.5.5 |
32+
| [remove-tabs](https://github.com/Lucas-C/pre-commit-hooks) | N/A | run tabs remover | find and convert tabs to spaces | v1.5.5 |
33+
| [actionlint](https://github.com/rhysd/actionlint) | N/A | run actionlint | lint GitHub Actions workflow files | v1.7.7 |
34+
| [codespell](https://github.com/codespell-project/codespell) | N/A | run codespell | check spelling with codespell | v2.4.1 |
35+
| [markdownlint](https://github.com/igorshubovych/markdownlint-cli) | N/A | run markdownlint | checks the style of Markdown files | v0.45.0 |
36+
| [markdown-link-check](https://github.com/tcort/markdown-link-check) | N/A | run markdown-link-check | checks hyperlinks in Markdown files | v3.13.7 |
37+
| [rubocop](https://github.com/rubocop/rubocop) | N/A | run rubocop | RuboCop is a Ruby code style checker (linter) and formatter based on the community-driven Ruby Style Guide | v1.78.0 |
38+
| [shellcheck](https://github.com/shellcheck-py/shellcheck-py) | N/A | run shellcheck | check shell scripts with a static analysis tool | v0.10.0.1 |
39+
| [yamllint](https://github.com/adrienverge/yamllint) | N/A | run yamllint | check YAML files with yamllint | v1.37.1 |

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pre-commit==4.2.0
2+
pyyaml==6.0.2

scripts/create_pre_commit_docs.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
3+
import yaml
4+
import sys # Import the sys module to access command-line arguments
5+
6+
# Define a constant for the default pre-commit config filename
7+
DEFAULT_PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml"
8+
9+
def generate_pre_commit_table(yaml_path):
10+
"""
11+
Generates a Markdown table from a pre-commit-config.yaml file.
12+
13+
Args:
14+
yaml_path (str): The path to the pre-commit-config.yaml file.
15+
16+
Returns:
17+
str: A Markdown formatted table string or an error message.
18+
"""
19+
try:
20+
with open(yaml_path, 'r', encoding='utf-8') as f: # Added encoding for better compatibility
21+
config = yaml.safe_load(f)
22+
except FileNotFoundError:
23+
return f"Error: The file '{yaml_path}' was not found."
24+
except yaml.YAMLError as e:
25+
return f"Error parsing YAML file '{yaml_path}': {e}"
26+
except Exception as e:
27+
return f"An unexpected error occurred while reading '{yaml_path}': {e}"
28+
29+
table_header = "| Hook ID | Language | Name | Description | Version |\n"
30+
table_separator = "|---|---|---|---|---|\n"
31+
table_rows = []
32+
33+
# Ensure config is a dictionary and has a 'repos' key that is a list
34+
if not isinstance(config, dict) or 'repos' not in config or not isinstance(config['repos'], list):
35+
return f"Error: Invalid pre-commit config structure in '{yaml_path}'. 'repos' section is missing or not a list."
36+
37+
for repo_index, repo in enumerate(config.get("repos", [])):
38+
if not isinstance(repo, dict):
39+
print(f"Warning: Repository at index {repo_index} is not a valid dictionary. Skipping.")
40+
continue
41+
42+
version = repo.get("rev", "N/A")
43+
url = repo.get("repo", "N/A")
44+
45+
if 'hooks' not in repo or not isinstance(repo['hooks'], list):
46+
print(
47+
f"Warning: 'hooks' section not found or is not a list for repo '{url}'. Skipping hooks processing for this repo.")
48+
continue
49+
50+
for hook_index, hook in enumerate(repo.get("hooks", [])):
51+
if not isinstance(hook, dict):
52+
print(f"Warning: Hook at index {hook_index} in repo '{url}' is not a valid dictionary. Skipping.")
53+
continue
54+
55+
hook_id = hook.get("id", "N/A")
56+
name = hook.get("name", "N/A")
57+
description = hook.get("description", "N/A")
58+
language = hook.get("language", "N/A")
59+
60+
# Construct the entry for the Hook ID column
61+
if url and url not in ["local", "meta"]:
62+
entry = f"[{hook_id}]({url})"
63+
else:
64+
entry = f"{hook_id}"
65+
66+
table_rows.append(f"| {entry} | {language} | {name} | {description} | {version} |\n")
67+
68+
if not table_rows:
69+
return f"No hooks found in '{yaml_path}' to generate a table."
70+
71+
return table_header + table_separator + "".join(table_rows)
72+
73+
74+
def create_markdown_file(target_file_path, content_to_append):
75+
"""
76+
Creates or overwrites a Markdown file with the provided content.
77+
78+
Args:
79+
target_file_path (str): The path to the output Markdown file.
80+
content_to_append (str): The Markdown content to write to the file.
81+
82+
Returns:
83+
str: A success message or an error message.
84+
"""
85+
try:
86+
# Ensure the directory exists before writing the file
87+
import os
88+
os.makedirs(os.path.dirname(target_file_path), exist_ok=True)
89+
90+
with open(target_file_path, "w", encoding='utf-8') as f: # Changed to 'w' to overwrite, added encoding
91+
f.write("# pre-commit hook documentation\n\n")
92+
f.write(content_to_append)
93+
return f"File content successfully created at '{target_file_path}'."
94+
except OSError as e:
95+
return f"Error creating file '{target_file_path}': {e}"
96+
except Exception as e:
97+
return f"An unexpected error occurred while writing to '{target_file_path}': {e}"
98+
99+
100+
if __name__ == "__main__":
101+
# Check if a command-line argument for the config file path is provided
102+
if len(sys.argv) > 1:
103+
pre_commit_yaml_path = sys.argv[1]
104+
else:
105+
# Default file path if no argument is provided
106+
pre_commit_yaml_path = DEFAULT_PRE_COMMIT_CONFIG_FILE
107+
print(f"No pre-commit config file path provided. Attempting to use default: '{pre_commit_yaml_path}'")
108+
109+
output_markdown_path = "doc/guides/pre-commit-hooks.md"
110+
111+
# Generate the Markdown table
112+
markdown_table = generate_pre_commit_table(pre_commit_yaml_path)
113+
114+
# Add the table to the target Markdown file if no error occurred during table generation
115+
if markdown_table.startswith("Error:"):
116+
print(markdown_table) # Print the error message from generate_pre_commit_table
117+
sys.exit(1) # Exit with a non-zero code to indicate failure
118+
elif markdown_table.startswith("No hooks found:"):
119+
print(markdown_table) # Print the message if no hooks are found
120+
sys.exit(0) # Exit with success if no hooks but no other error
121+
else:
122+
result = create_markdown_file(output_markdown_path, markdown_table)
123+
print(result)
124+
if "Error" in result:
125+
sys.exit(1) # Exit with failure if create_markdown_file had an error
126+
else:
127+
sys.exit(0) # Exit with success
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
3+
import yaml
4+
import sys
5+
6+
# Define a constant for the default pre-commit config filename
7+
DEFAULT_PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml"
8+
9+
def validate_pre_commit_config(file_path):
10+
"""
11+
Validates a pre-commit-config.yaml file to ensure all hooks have
12+
'name' and 'description' keys.
13+
14+
Args:
15+
file_path (str): The path to the pre-commit-config.yaml file.
16+
17+
Returns:
18+
bool: True if all hooks are valid, False otherwise.
19+
"""
20+
try:
21+
with open(file_path, 'r', encoding='utf-8') as f:
22+
config = yaml.safe_load(f)
23+
except FileNotFoundError:
24+
print(f"Error: The file '{file_path}' was not found.")
25+
return False
26+
except yaml.YAMLError as e:
27+
print(f"Error: Could not parse YAML file '{file_path}'. Please check its syntax.")
28+
print(f"Details: {e}")
29+
return False
30+
except Exception as e:
31+
print(f"An unexpected error occurred while reading the file: {e}")
32+
return False
33+
34+
if not isinstance(config, dict):
35+
print(f"Error: The content of '{file_path}' is not a valid YAML dictionary.")
36+
return False
37+
38+
if 'repos' not in config or not isinstance(config['repos'], list):
39+
print(f"Error: 'repos' section not found or is not a list in '{file_path}'.")
40+
return False
41+
42+
all_hooks_valid = True
43+
for repo_index, repo in enumerate(config['repos']):
44+
if not isinstance(repo, dict):
45+
print(f"Warning: Repository at index {repo_index} is not a valid dictionary. Skipping.")
46+
all_hooks_valid = False
47+
continue
48+
49+
repo_url = repo.get('repo', 'Unknown Repo URL')
50+
51+
if 'hooks' not in repo or not isinstance(repo['hooks'], list):
52+
print(
53+
f"Warning: 'hooks' section not found or is not a list for repo '{repo_url}'. Skipping hooks validation for this repo.")
54+
all_hooks_valid = False
55+
continue
56+
57+
for hook_index, hook in enumerate(repo['hooks']):
58+
if not isinstance(hook, dict):
59+
print(f"Warning: Hook at index {hook_index} in repo '{repo_url}' is not a valid dictionary. Skipping.")
60+
all_hooks_valid = False
61+
continue
62+
63+
hook_id = hook.get('id', 'Unknown ID')
64+
65+
# Check for 'name' key
66+
if 'name' not in hook:
67+
print(
68+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' is missing the 'name' key.")
69+
all_hooks_valid = False
70+
elif not isinstance(hook['name'], str) or not hook['name'].strip():
71+
print(
72+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' has an empty or invalid 'name' key.")
73+
all_hooks_valid = False
74+
75+
# Check for 'description' key
76+
if 'description' not in hook:
77+
print(
78+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' is missing the 'description' key.")
79+
all_hooks_valid = False
80+
elif not isinstance(hook['description'], str) or not hook['description'].strip():
81+
print(
82+
f"Validation Error: Hook '{hook_id}' (index {hook_index}) in repo '{repo_url}' has an empty or invalid 'description' key.")
83+
all_hooks_valid = False
84+
85+
return all_hooks_valid
86+
87+
88+
if __name__ == "__main__":
89+
# Check if a command-line argument for the config file path is provided
90+
if len(sys.argv) > 1:
91+
config_file = sys.argv[1]
92+
else:
93+
# Use the defined default file path if no argument is provided
94+
config_file = DEFAULT_PRE_COMMIT_CONFIG_FILE
95+
print(f"No file path provided. Attempting to validate default file: '{config_file}'")
96+
97+
if validate_pre_commit_config(config_file):
98+
print(f"\nValidation successful! All hooks in '{config_file}' have 'name' and 'description' keys.")
99+
else:
100+
print(f"\nValidation failed. Please check the errors listed above for '{config_file}'.")
101+
sys.exit(1) # Exit with a non-zero code to indicate failure

0 commit comments

Comments
 (0)