php8_compat.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """PostToolUse hook (GLOBAL): 写 .php 后做 PHP 8.x 兼容性静态扫描(对齐 Rules §6)。
  4. 仅做纯模式匹配(不依赖 php -l,因本环境 php -l 退出码不可信),命中即 warning 回显 AI,
  5. 非阻断(exit 0)。涵盖:短标签、裸数组键、each()/create_function()/get_magic_quotes_gpc()。
  6. """
  7. import sys
  8. import os
  9. import json
  10. import re
  11. # <? 后非 php/=/xml 视为短标签(<?php / <?= / <?xml 合法)
  12. SHORT_TAG = re.compile(r'<\?(?!(php|=|xml)\b)')
  13. # $arr[key] 裸键(key 为字母开头、非 $ 变量、非引号、非纯数字)
  14. BARE_KEY = re.compile(r'\$\w+\[([A-Za-z_][A-Za-z0-9_]*)\]')
  15. BAD_FUNCS = [
  16. (re.compile(r'\beach\s*\('), 'each()'),
  17. (re.compile(r'\bcreate_function\s*\('), 'create_function()'),
  18. (re.compile(r'\bget_magic_quotes_gpc\s*\('), 'get_magic_quotes_gpc()'),
  19. ]
  20. def scan(text):
  21. hits = []
  22. if SHORT_TAG.search(text):
  23. hits.append("短标签 <?(非 <?php/<?=/<?xml),须改 <?php")
  24. mk = BARE_KEY.search(text)
  25. if mk:
  26. key = mk.group(1)
  27. # 跳过全大写常量(如 $arr[MY_CONST])及语言常量 true/false/null,
  28. # 避免误伤合法常量用法;§6 针对的是裸字符串键(小写/混合大小写)
  29. if key.isupper() or key.lower() in ("true", "false", "null"):
  30. pass
  31. else:
  32. hits.append("裸数组键 $arr[%s] 未加引号,须 $arr['%s'](Rules §6)" % (key, key))
  33. for rx, name in BAD_FUNCS:
  34. if rx.search(text):
  35. hits.append("已废弃函数 %s(PHP8 已移除),须替换(Rules §6)" % name)
  36. return hits
  37. def main():
  38. try:
  39. raw = sys.stdin.read()
  40. if not raw.strip():
  41. return 0
  42. data = json.loads(raw)
  43. except Exception:
  44. return 0
  45. tool_input = data.get("tool_input", {}) or {}
  46. path = (
  47. tool_input.get("filePath")
  48. or tool_input.get("file_path")
  49. or tool_input.get("path")
  50. or ""
  51. )
  52. if not path:
  53. return 0
  54. path = os.path.abspath(path)
  55. if os.path.splitext(path)[1].lower() != ".php":
  56. return 0
  57. if not os.path.isfile(path):
  58. return 0
  59. try:
  60. text = open(path, encoding="utf-8", errors="ignore").read()
  61. except Exception:
  62. return 0
  63. hits = scan(text)
  64. if hits:
  65. print("[PHP8-COMPAT-WARN] 发现 PHP8 兼容隐患:")
  66. for h in hits:
  67. print(" - " + h)
  68. return 0
  69. if __name__ == "__main__":
  70. sys.exit(main())