@sun-asterisk/sunlint 1.3.48 → 1.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +1 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: Specify Encoding When Opening Files
3
+ impact: MEDIUM
4
+ impactDescription: Omitting encoding when opening text files causes silent failures on systems with non-UTF-8 locale, resulting in UnicodeDecodeError or garbled data in production.
5
+ tags: python, encoding, files, portability, quality
6
+ ---
7
+
8
+ ## Specify Encoding When Opening Files
9
+
10
+ When calling `open()` in text mode without an explicit `encoding` argument, Python uses the OS's default locale encoding. This is typically UTF-8 on Linux/macOS, but `cp1252` or similar on Windows. Code that works locally can silently fail or corrupt data in other environments.
11
+
12
+ Always pass `encoding="utf-8"` (or whatever the file format requires) explicitly.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ # Relies on OS default — breaks on Windows or non-UTF-8 systems
17
+ with open("data.txt") as f:
18
+ content = f.read()
19
+
20
+ with open("output.csv", "w") as f:
21
+ f.write("café,prix\n")
22
+ ```
23
+
24
+ **Correct:**
25
+ ```python
26
+ with open("data.txt", encoding="utf-8") as f:
27
+ content = f.read()
28
+
29
+ with open("output.csv", "w", encoding="utf-8") as f:
30
+ f.write("café,prix\n")
31
+
32
+ # When reading binary, no encoding needed
33
+ with open("image.png", "rb") as f:
34
+ data = f.read()
35
+ ```
36
+
37
+ **Pathlib equivalent (also requires explicit encoding):**
38
+ ```python
39
+ from pathlib import Path
40
+
41
+ content = Path("data.txt").read_text(encoding="utf-8")
42
+ Path("output.txt").write_text("hello", encoding="utf-8")
43
+ ```
44
+
45
+ **Tools:** Ruff `W1514` / `PLW1514` (unspecified-encoding), Pylint `W1514`, flake8-bugbear
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Use Context Manager for File and Resource Handling
3
+ impact: MEDIUM
4
+ impactDescription: Not using with statement for file/network/lock resources causes resource leaks when exceptions are raised, leading to open file handles, locked databases, and memory exhaustion.
5
+ tags: python, resources, context-manager, files, quality
6
+ ---
7
+
8
+ ## Use Context Manager for File and Resource Handling
9
+
10
+ Python's `with` statement (context manager protocol) guarantees that resources are released even when exceptions occur. Always use `with` when dealing with files, network connections, database cursors, threading locks, and any object that implements `__enter__`/`__exit__`.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ # File handle not closed if exception occurs
15
+ f = open("data.txt", encoding="utf-8")
16
+ content = f.read() # If this raises, f.close() is never called
17
+ f.close()
18
+
19
+ # Lock never released if exception raised inside
20
+ lock = threading.Lock()
21
+ lock.acquire()
22
+ do_work() # Exception here leaves lock permanently acquired
23
+ lock.release()
24
+ ```
25
+
26
+ **Correct:**
27
+ ```python
28
+ # File is always closed, even on exception
29
+ with open("data.txt", encoding="utf-8") as f:
30
+ content = f.read()
31
+
32
+ # Lock always released
33
+ import threading
34
+
35
+ lock = threading.Lock()
36
+ with lock:
37
+ do_work()
38
+
39
+ # Multiple context managers in one with
40
+ with open("input.txt", encoding="utf-8") as fin, \
41
+ open("output.txt", "w", encoding="utf-8") as fout:
42
+ fout.write(fin.read())
43
+ ```
44
+
45
+ **contextlib.suppress as context manager:**
46
+ ```python
47
+ import contextlib
48
+
49
+ # Instead of try/except/pass
50
+ with contextlib.suppress(FileNotFoundError):
51
+ os.remove("temp.txt")
52
+ ```
53
+
54
+ **Tools:** Ruff `SIM115` (open-file-with-context-handler), `R1732` (consider-using-with), Pylint, flake8-simplify
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Never Use Bare except Clause
3
+ impact: HIGH
4
+ impactDescription: A bare except catches SystemExit and KeyboardInterrupt, making programs impossible to interrupt and hiding unrelated errors that should propagate.
5
+ tags: python, exceptions, error-handling, pitfalls, quality
6
+ ---
7
+
8
+ ## Never Use Bare except Clause
9
+
10
+ A bare `except:` clause (with no exception type specified) catches **every** exception, including `SystemExit` (raised by `sys.exit()`), `KeyboardInterrupt` (Ctrl+C), and `GeneratorExit`. This prevents programs from being terminated normally, swallows programming errors, and makes debugging extremely difficult.
11
+
12
+ Always specify the exception type(s) you intend to handle.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ # Catches SystemExit and KeyboardInterrupt — program cannot be stopped
17
+ try:
18
+ process_data()
19
+ except:
20
+ print("Something went wrong")
21
+
22
+ # Also problematic: too broad
23
+ try:
24
+ connect_to_db()
25
+ except Exception:
26
+ pass # silently ignores all errors including programming mistakes
27
+ ```
28
+
29
+ **Correct:**
30
+ ```python
31
+ # Catch only what you expect and can handle
32
+ try:
33
+ process_data()
34
+ except ValueError as e:
35
+ logger.warning("Invalid data: %s", e)
36
+ except OSError as e:
37
+ logger.error("I/O error during processing: %s", e)
38
+
39
+ # If you need a catch-all, at least log it and re-raise
40
+ try:
41
+ connect_to_db()
42
+ except Exception as e:
43
+ logger.error("Unexpected error connecting to DB: %s", e, exc_info=True)
44
+ raise
45
+
46
+ # Correct way to suppress a specific expected error
47
+ import contextlib
48
+
49
+ with contextlib.suppress(FileNotFoundError):
50
+ os.remove("temp.txt")
51
+ ```
52
+
53
+ **Exception hierarchy to catch:**
54
+ ```python
55
+ # Prefer specific → broad order
56
+ try:
57
+ result = int(user_input)
58
+ except ValueError:
59
+ result = 0 # handle invalid literal
60
+ except (TypeError, OverflowError) as e:
61
+ logger.warning("Type issue: %s", e)
62
+ result = 0
63
+ ```
64
+
65
+ **Tools:** Ruff `E722` (bare-except), `W0702` in Pylint, `BLE001` (blind-except), flake8, pyflakes
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: Use isinstance() Instead of type() for Type Checking
3
+ impact: MEDIUM
4
+ impactDescription: Using type() == for type checks breaks polymorphism and inheritance, rejecting valid subclass instances that should be accepted by the contract.
5
+ tags: python, typing, isinstance, quality, oop
6
+ ---
7
+
8
+ ## Use isinstance() Instead of type() for Type Checking
9
+
10
+ The `type(x) == SomeClass` pattern performs an exact type match and does not account for subclasses. This violates the Liskov Substitution Principle: code that accepts a `list` should also accept `UserList` or any other list subclass.
11
+
12
+ Use `isinstance()` which checks the full MRO (method resolution order) and correctly handles subclasses and abstract base classes.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ def process(data):
17
+ if type(data) == list: # rejects OrderedList, UserList, etc.
18
+ for item in data:
19
+ handle(item)
20
+ if type(data) == dict: # rejects defaultdict, OrderedDict, etc.
21
+ process_mapping(data)
22
+
23
+ def serialize(value):
24
+ if type(value) == str: # rejects str subclasses
25
+ return value.encode("utf-8")
26
+ ```
27
+
28
+ **Correct:**
29
+ ```python
30
+ def process(data):
31
+ if isinstance(data, list): # accepts all list subclasses
32
+ for item in data:
33
+ handle(item)
34
+ if isinstance(data, dict): # accepts defaultdict, OrderedDict, etc.
35
+ process_mapping(data)
36
+
37
+ def serialize(value):
38
+ if isinstance(value, str):
39
+ return value.encode("utf-8")
40
+
41
+ # Use abstract base classes for duck typing
42
+ from collections.abc import Mapping, Sequence
43
+
44
+ def process_generic(data):
45
+ if isinstance(data, Sequence): # list, tuple, str, UserList, etc.
46
+ for item in data:
47
+ handle(item)
48
+ if isinstance(data, Mapping): # dict, defaultdict, ChainMap, etc.
49
+ process_mapping(data)
50
+ ```
51
+
52
+ **Exception — when exact type match IS needed:**
53
+ ```python
54
+ # Only use type() == when you explicitly want to exclude subclasses
55
+ # e.g., in a serialization library distinguishing bool from int:
56
+ if type(value) is bool: # True/False, not int subclasses
57
+ return str(value).lower()
58
+ ```
59
+
60
+ **Tools:** Ruff `E721` (type-comparison), Pylint `C0123` (unidiomatic-typecheck), mypy, pyright
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Always Use Timezone-Aware Datetimes
3
+ impact: HIGH
4
+ impactDescription: Naive datetimes (without tzinfo) cause silent bugs when comparing times across timezones, leading to incorrect scheduling, expiry calculations, and audit logs.
5
+ tags: python, datetime, timezone, bugs, quality
6
+ ---
7
+
8
+ ## Always Use Timezone-Aware Datetimes
9
+
10
+ Python's `datetime` objects can be *naive* (no timezone) or *aware* (with timezone). Mixing naive and aware datetimes raises a `TypeError`, but naive datetimes used consistently silently produce wrong results when code runs in different timezones (CI server vs production, container vs host).
11
+
12
+ Always create timezone-aware datetimes by passing `tz` or `tzinfo`.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ from datetime import datetime
17
+
18
+ # Naive datetimes — "now" means different things in Tokyo vs London
19
+ created_at = datetime.now()
20
+ expires_at = datetime.utcnow() # UTC but naive — still dangerous
21
+
22
+ # Comparing naive datetimes assumes both are in same timezone (often wrong)
23
+ if datetime.now() > token_expiry: # fails if token_expiry is aware
24
+ raise TokenExpiredError()
25
+ ```
26
+
27
+ **Correct:**
28
+ ```python
29
+ from datetime import datetime, timezone
30
+
31
+ # Python 3.11+: use datetime.UTC constant
32
+ now = datetime.now(tz=timezone.utc)
33
+
34
+ # Python 3.9+: use zoneinfo for local zones
35
+ from zoneinfo import ZoneInfo
36
+
37
+ jst_now = datetime.now(tz=ZoneInfo("Asia/Tokyo"))
38
+ utc_now = datetime.now(tz=timezone.utc)
39
+
40
+ # django/pytz style (pre-3.9)
41
+ import pytz
42
+
43
+ utc_now = datetime.now(tz=pytz.utc)
44
+
45
+ # Always compare aware with aware
46
+ token_expiry: datetime # must be aware
47
+ if datetime.now(tz=timezone.utc) > token_expiry:
48
+ raise TokenExpiredError()
49
+ ```
50
+
51
+ **Converting naive to aware (migration):**
52
+ ```python
53
+ # Assume a legacy naive datetime is UTC — localize it explicitly
54
+ naive_dt = datetime(2024, 1, 15, 10, 30)
55
+ aware_dt = naive_dt.replace(tzinfo=timezone.utc)
56
+ ```
57
+
58
+ **Tools:** Ruff `DTZ001`–`DTZ012` (flake8-datetimez), Pylint `W1502`, mypy with `--strict-optional`
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Use pathlib Instead of os.path for File Operations
3
+ impact: MEDIUM
4
+ impactDescription: os.path functions return plain strings that are easy to misuse; pathlib.Path provides an object-oriented, composable, and cross-platform API that eliminates common path manipulation bugs.
5
+ tags: python, pathlib, os-path, files, quality, python3
6
+ ---
7
+
8
+ ## Use pathlib Instead of os.path for File Operations
9
+
10
+ Python 3.4 introduced `pathlib.Path` as a modern, object-oriented replacement for `os.path`. Paths are represented as objects supporting `/` operator for joining, `.stem`, `.suffix`, `.parent` for decomposition, and `.read_text()` / `.write_text()` for content — with no risk of forgetting `os.path.join` and accidentally concatenating strings.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ import os
15
+
16
+ # String concatenation — silent bugs on Windows with backslashes
17
+ config_path = base_dir + "/config/" + "settings.json"
18
+
19
+ # Verbose os.path usage
20
+ import os.path
21
+
22
+ full_path = os.path.join(base_dir, "config", "settings.json")
23
+ file_name = os.path.basename(full_path)
24
+ dir_name = os.path.dirname(full_path)
25
+ stem = os.path.splitext(file_name)[0]
26
+ ext = os.path.splitext(file_name)[1]
27
+
28
+ if os.path.exists(config_path):
29
+ with open(config_path, encoding="utf-8") as f:
30
+ content = f.read()
31
+
32
+ os.makedirs(os.path.join(base_dir, "logs"), exist_ok=True)
33
+ ```
34
+
35
+ **Correct:**
36
+ ```python
37
+ from pathlib import Path
38
+
39
+ base_dir = Path("/app")
40
+
41
+ # / operator for joining — cross-platform and clear
42
+ config_path = base_dir / "config" / "settings.json"
43
+
44
+ # Readable decomposition
45
+ file_name = config_path.name # "settings.json"
46
+ dir_name = config_path.parent # Path("/app/config")
47
+ stem = config_path.stem # "settings"
48
+ ext = config_path.suffix # ".json"
49
+
50
+ # Built-in existence check and file reading
51
+ if config_path.exists():
52
+ content = config_path.read_text(encoding="utf-8")
53
+
54
+ # Directory creation
55
+ (base_dir / "logs").mkdir(parents=True, exist_ok=True)
56
+
57
+ # Glob for files
58
+ for py_file in base_dir.rglob("*.py"):
59
+ print(py_file)
60
+ ```
61
+
62
+ **Tools:** Ruff `PTH100`–`PTH208` (flake8-use-pathlib), pyupgrade, SonarQube Python rules
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: Never Use Wildcard Imports
3
+ impact: HIGH
4
+ impactDescription: Wildcard imports pollute the namespace, make it impossible to trace where a name comes from, and can silently override existing names, causing hard-to-debug subtle bugs.
5
+ tags: python, imports, namespace, quality, readability
6
+ ---
7
+
8
+ ## Never Use Wildcard Imports
9
+
10
+ `from module import *` imports every public name from a module into the current namespace. This:
11
+ - Makes it impossible to know where any name is defined without reading the imported module
12
+ - Can silently override names from prior imports or `builtins`
13
+ - Prevents static analysis tools from detecting undefined names
14
+ - Breaks auto-complete and go-to-definition in IDEs
15
+
16
+ The only acceptable use of `import *` is in a package's `__init__.py` to re-export a curated public API defined in `__all__`.
17
+
18
+ **Incorrect:**
19
+ ```python
20
+ from os.path import * # which names are now in scope?
21
+ from numpy import * # overrides Python's built-in sum, any, all, etc.
22
+ from models import * # User? Order? both? neither?
23
+ from utils import *
24
+
25
+ # Now these silently shadow built-ins:
26
+ result = sum([1, 2, 3]) # which sum? Python's or numpy's?
27
+ ```
28
+
29
+ **Correct:**
30
+ ```python
31
+ import os
32
+ from os import path as osp # or use pathlib
33
+ from pathlib import Path
34
+ import numpy as np # conventional alias
35
+ from models import User, Order, Product # explicit names
36
+ from utils import format_date, validate_email
37
+
38
+ # Clear provenance for every name
39
+ result = np.sum([1, 2, 3])
40
+ full_path = Path("/data") / "file.txt"
41
+ ```
42
+
43
+ **Acceptable use in `__init__.py`:**
44
+ ```python
45
+ # package/__init__.py — re-export public API
46
+ from .client import Client
47
+ from .exceptions import APIError, RateLimitError
48
+
49
+ __all__ = ["Client", "APIError", "RateLimitError"]
50
+ ```
51
+
52
+ **Tools:** Ruff `F403` (undefined-local-with-import-star), `W0401` in Pylint (wildcard-import), flake8, isort
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: Use Lazy Formatting in Logging Calls
3
+ impact: MEDIUM
4
+ impactDescription: Eager string interpolation in logging (f-strings or %) evaluates and allocates the string even when the log level is disabled, wasting CPU and memory in production.
5
+ tags: python, logging, performance, quality
6
+ ---
7
+
8
+ ## Use Lazy Formatting in Logging Calls
9
+
10
+ Python's `logging` module accepts a format string and arguments **separately**. The string interpolation only happens if the message will actually be emitted (i.e., the log level is enabled). Using f-strings or `%` interpolation beforehand evaluates the string unconditionally, even if `DEBUG` is disabled.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ user_id = get_user_id()
19
+ payload = serialize(data)
20
+
21
+ # These always evaluate the f-string / % formatting, even if DEBUG is off
22
+ logger.debug(f"Processing user {user_id} with payload {payload}")
23
+ logger.info("Request from %s: %s" % (ip_address, request.path))
24
+ logger.warning("Slow query: " + str(query_time) + "ms")
25
+ ```
26
+
27
+ **Correct:**
28
+ ```python
29
+ import logging
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Formatting deferred — only if DEBUG is enabled
34
+ logger.debug("Processing user %s with payload %s", user_id, payload)
35
+ logger.info("Request from %s: %s", ip_address, request.path)
36
+ logger.warning("Slow query: %sms", query_time)
37
+
38
+ # For complex objects, use lazy repr via %r
39
+ logger.debug("Event received: %r", event)
40
+
41
+ # Exception info — use exc_info=True or logger.exception()
42
+ try:
43
+ call_external_api()
44
+ except requests.RequestException as e:
45
+ logger.error("External API failed for user %s: %s", user_id, e, exc_info=True)
46
+ # or equivalently inside an except block:
47
+ # logger.exception("External API failed for user %s", user_id)
48
+ ```
49
+
50
+ **Tools:** Ruff `G004` (logging-f-string), `W1201` in Pylint (logging-not-lazy), `W1202` (logging-format-interpolation), `TRY400` (error-instead-of-exception), flake8-logging-format
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Use raise...from to Chain Exceptions
3
+ impact: MEDIUM
4
+ impactDescription: Rethrowing an exception without raise...from loses the original traceback, making it impossible to diagnose the root cause in production logs.
5
+ tags: python, exceptions, error-handling, traceability, quality
6
+ ---
7
+
8
+ ## Use raise...from to Chain Exceptions
9
+
10
+ When catching an exception and raising a different (or more specific) one, use `raise NewException("msg") from original_exception` to preserve the full causal chain. Without `from`, Python 3 still shows an implicit chain, but the context can be confusing. Using `from` makes the chain explicit and intentional.
11
+
12
+ Using `raise ... from None` deliberately suppresses the original context when it adds no value to the end user.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ import json
17
+
18
+ def load_config(path: str) -> dict:
19
+ try:
20
+ with open(path, encoding="utf-8") as f:
21
+ return json.load(f)
22
+ except json.JSONDecodeError:
23
+ raise ValueError("Config file is malformed") # original traceback lost
24
+
25
+ def get_user(user_id: int) -> dict:
26
+ try:
27
+ return db.find_by_id(user_id)
28
+ except DatabaseError:
29
+ raise RuntimeError("Failed to fetch user") # loses DB error context
30
+ ```
31
+
32
+ **Correct:**
33
+ ```python
34
+ import json
35
+
36
+ def load_config(path: str) -> dict:
37
+ try:
38
+ with open(path, encoding="utf-8") as f:
39
+ return json.load(f)
40
+ except json.JSONDecodeError as e:
41
+ raise ValueError(f"Config file '{path}' is malformed") from e # chain preserved
42
+
43
+ def get_user(user_id: int) -> dict:
44
+ try:
45
+ return db.find_by_id(user_id)
46
+ except DatabaseError as e:
47
+ raise UserNotFoundError(f"User {user_id} could not be fetched") from e
48
+
49
+ # Suppress context intentionally (e.g., to hide internal DB details from callers)
50
+ def validate_token(token: str) -> None:
51
+ try:
52
+ jwt.decode(token, SECRET)
53
+ except jwt.ExpiredSignatureError:
54
+ raise AuthenticationError("Token has expired") from None
55
+ ```
56
+
57
+ **Tools:** Ruff `W0707` / `raise-missing-from`, `TRY200` (reraise-no-cause), `B904` (raise-without-from-inside-except), Pylint `W0707`, flake8-bugbear
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Pass Explicit check= to subprocess.run
3
+ impact: HIGH
4
+ impactDescription: subprocess.run silently ignores non-zero exit codes by default; a failed command goes undetected and subsequent code runs on corrupt or missing output.
5
+ tags: python, subprocess, error-handling, quality, security
6
+ ---
7
+
8
+ ## Pass Explicit check= to subprocess.run
9
+
10
+ `subprocess.run()` has `check=False` by default, meaning a command that exits with a non-zero status (indicating failure) does not raise an exception. Code that follows assumes success and may operate on missing or corrupt output without any error ever being raised.
11
+
12
+ Always pass `check=True` unless you explicitly intend to handle failures yourself by inspecting `returncode`.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ import subprocess
17
+
18
+ # Exit code ignored — if ffmpeg fails, output file doesn't exist
19
+ subprocess.run(["ffmpeg", "-i", "input.mp4", "output.mp4"])
20
+
21
+ # result.returncode is never checked
22
+ result = subprocess.run(["git", "pull"])
23
+ print("Done") # runs even if git pull failed
24
+
25
+ # capture_output=True but still no check
26
+ result = subprocess.run(
27
+ ["python", "migrate.py"],
28
+ capture_output=True,
29
+ text=True,
30
+ )
31
+ apply_migrations() # runs even if migrate.py failed
32
+ ```
33
+
34
+ **Correct:**
35
+ ```python
36
+ import subprocess
37
+
38
+ # Raises CalledProcessError if command fails
39
+ subprocess.run(
40
+ ["ffmpeg", "-i", "input.mp4", "output.mp4"],
41
+ check=True,
42
+ )
43
+
44
+ # Or capture output and check explicitly
45
+ result = subprocess.run(
46
+ ["python", "migrate.py"],
47
+ capture_output=True,
48
+ text=True,
49
+ check=True, # raises on non-zero exit code
50
+ )
51
+ apply_migrations() # only reached if migration succeeded
52
+
53
+ # When you WANT to handle failures yourself — explicit intent:
54
+ result = subprocess.run(["git", "pull"], check=False)
55
+ if result.returncode != 0:
56
+ logger.warning("git pull failed with code %d", result.returncode)
57
+ ```
58
+
59
+ **Tools:** Ruff `PLW1510` (subprocess-run-without-check), Pylint `W1510`, Bandit `S603`, flake8-bugbear
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: Always Set timeout for HTTP Requests
3
+ impact: HIGH
4
+ impactDescription: HTTP requests without a timeout can hang indefinitely, exhausting thread pools and connection pools, causing cascading failures and outages in production services.
5
+ tags: python, requests, http, reliability, quality
6
+ ---
7
+
8
+ ## Always Set timeout for HTTP Requests
9
+
10
+ The `requests` library and similar HTTP clients have **no default timeout**. A single slow or unresponsive server can cause a thread to block indefinitely, exhausting thread pools and causing the entire service to become unresponsive. Always set an explicit `timeout` on every outbound HTTP call.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ import requests
15
+
16
+ # Hangs forever if server is unresponsive
17
+ response = requests.get("https://api.example.com/data")
18
+
19
+ # Also no timeout
20
+ session = requests.Session()
21
+ response = session.post(
22
+ "https://api.example.com/webhook",
23
+ json=payload,
24
+ )
25
+
26
+ # httpx — same risk
27
+ import httpx
28
+ response = httpx.get("https://api.example.com/items")
29
+ ```
30
+
31
+ **Correct:**
32
+ ```python
33
+ import requests
34
+
35
+ # Set both connect and read timeouts as a tuple
36
+ response = requests.get(
37
+ "https://api.example.com/data",
38
+ timeout=(3.05, 27), # (connect_timeout, read_timeout) in seconds
39
+ )
40
+
41
+ # Or a single value for both
42
+ response = requests.post(
43
+ "https://api.example.com/webhook",
44
+ json=payload,
45
+ timeout=10, # applies to both connect and read
46
+ )
47
+ response.raise_for_status()
48
+
49
+ # httpx — also requires explicit timeout
50
+ import httpx
51
+
52
+ with httpx.Client(timeout=httpx.Timeout(connect=3.0, read=30.0)) as client:
53
+ response = client.get("https://api.example.com/items")
54
+ ```
55
+
56
+ **Configure timeouts at the session level:**
57
+ ```python
58
+ # Apply uniformly to all requests from the session
59
+ session = requests.Session()
60
+ adapter = requests.adapters.HTTPAdapter(max_retries=3)
61
+ session.mount("https://", adapter)
62
+
63
+ DEFAULT_TIMEOUT = (3.05, 27)
64
+
65
+ def get_with_timeout(url: str, **kwargs) -> requests.Response:
66
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
67
+ return session.get(url, **kwargs)
68
+ ```
69
+
70
+ **Tools:** Ruff `W3101` (missing-timeout), Pylint `W3101`, Bandit `S113`, flake8