select_star.py 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """PostToolUse hook (GLOBAL): 写源码后扫描 SELECT * 裸查询。
  4. 对齐 Rules §10 性能禁则(禁止 SELECT *,须显式列名)。仅对文本源文件生效。
  5. 非阻断 exit 1 回显 AI。SELECT 与 * 跨行(\\s+)亦可识别;count(*) 不含 'select *' 不误报。
  6. """
  7. import sys
  8. import os
  9. import json
  10. import re
  11. TEXT_EXT = {".php", ".html", ".htm", ".js", ".css", ".py", ".sql", ".inc"}
  12. SELECT_STAR = re.compile(r"select\s+\*", re.IGNORECASE)
  13. def find_select_star_lines(text):
  14. """逐行扫描,跳过 PHP/JS/CSS 注释(//、#、/* */),返回命中行号。
  15. 真实 SQL 多位于字符串字面量所在代码行,仍会被捕获;仅排除注释行,
  16. 以降低说明性注释里出现 select * 的误报。
  17. """
  18. hits = []
  19. in_block = False
  20. for i, line in enumerate(text.splitlines(), start=1):
  21. s = line.strip()
  22. if in_block:
  23. if "*/" in line:
  24. in_block = False
  25. continue
  26. if "/*" in line:
  27. if "*/" in line:
  28. continue # 同行闭合块注释,整行视为注释
  29. in_block = True
  30. continue
  31. if s.startswith("//") or s.startswith("#"):
  32. continue
  33. if SELECT_STAR.search(line):
  34. hits.append(i)
  35. return hits
  36. def main():
  37. try:
  38. raw = sys.stdin.read()
  39. if not raw.strip():
  40. return 0
  41. data = json.loads(raw)
  42. except Exception:
  43. return 0
  44. tool_input = data.get("tool_input", {}) or {}
  45. path = (
  46. tool_input.get("filePath")
  47. or tool_input.get("file_path")
  48. or tool_input.get("path")
  49. or ""
  50. )
  51. if not path:
  52. return 0
  53. path = os.path.abspath(path)
  54. if os.path.splitext(path)[1].lower() not in TEXT_EXT:
  55. return 0
  56. if not os.path.isfile(path):
  57. return 0
  58. try:
  59. with open(path, "r", encoding="utf-8", errors="ignore") as f:
  60. text = f.read()
  61. except Exception:
  62. return 0
  63. hits = find_select_star_lines(text)
  64. if hits:
  65. print("[SQL-WARN] 发现 SELECT * 裸查询,请改为显式列名(Rules §10 性能禁则):")
  66. for line in hits:
  67. print(" 行 %d" % line)
  68. return 1
  69. return 0
  70. if __name__ == "__main__":
  71. sys.exit(main())