1""" 2EXEC: Ensure that source files are not executable. 3""" 4 5from __future__ import annotations 6 7import argparse 8import json 9import logging 10import os 11import sys 12from enum import Enum 13from typing import NamedTuple 14 15 16LINTER_CODE = "EXEC" 17 18 19class LintSeverity(str, Enum): 20 ERROR = "error" 21 WARNING = "warning" 22 ADVICE = "advice" 23 DISABLED = "disabled" 24 25 26class LintMessage(NamedTuple): 27 path: str | None 28 line: int | None 29 char: int | None 30 code: str 31 severity: LintSeverity 32 name: str 33 original: str | None 34 replacement: str | None 35 description: str | None 36 37 38def check_file(filename: str) -> LintMessage | None: 39 is_executable = os.access(filename, os.X_OK) 40 if is_executable: 41 return LintMessage( 42 path=filename, 43 line=None, 44 char=None, 45 code=LINTER_CODE, 46 severity=LintSeverity.ERROR, 47 name="executable-permissions", 48 original=None, 49 replacement=None, 50 description="This file has executable permission; please remove it by using `chmod -x`.", 51 ) 52 return None 53 54 55if __name__ == "__main__": 56 parser = argparse.ArgumentParser( 57 description="exec linter", 58 fromfile_prefix_chars="@", 59 ) 60 parser.add_argument( 61 "--verbose", 62 action="store_true", 63 ) 64 parser.add_argument( 65 "filenames", 66 nargs="+", 67 help="paths to lint", 68 ) 69 70 args = parser.parse_args() 71 72 logging.basicConfig( 73 format="<%(threadName)s:%(levelname)s> %(message)s", 74 level=logging.NOTSET 75 if args.verbose 76 else logging.DEBUG 77 if len(args.filenames) < 1000 78 else logging.INFO, 79 stream=sys.stderr, 80 ) 81 82 lint_messages = [] 83 for filename in args.filenames: 84 lint_message = check_file(filename) 85 if lint_message is not None: 86 lint_messages.append(lint_message) 87 88 for lint_message in lint_messages: 89 print(json.dumps(lint_message._asdict()), flush=True) 90