@sun-asterisk/sunlint 1.3.47 → 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.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Service Layer — Controllers Must Not Contain Business Logic
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: fat controllers are untestable, unmaintainable, and couple HTTP concerns with domain logic
|
|
5
|
+
tags: service-layer, architecture, clean-code, controller, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Service Layer — Controllers Must Not Contain Business Logic
|
|
9
|
+
|
|
10
|
+
Controllers handle HTTP: parse request, call service, return response. Domain logic (calculations, business rules, orchestration) belongs in a dedicated Service class.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// OrderController doing everything — HTTP + business + DB
|
|
16
|
+
public function store(StoreOrderRequest $request)
|
|
17
|
+
{
|
|
18
|
+
$subtotal = 0;
|
|
19
|
+
foreach ($request->items as $item) {
|
|
20
|
+
$product = Product::find($item['product_id']);
|
|
21
|
+
if ($product->stock < $item['qty']) {
|
|
22
|
+
return response()->json(['error' => 'Insufficient stock'], 422);
|
|
23
|
+
}
|
|
24
|
+
$subtotal += $product->price * $item['qty'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$discount = $request->user()->isPremium() ? $subtotal * 0.1 : 0;
|
|
28
|
+
$total = $subtotal - $discount + $this->calculateShipping($request->address);
|
|
29
|
+
|
|
30
|
+
$order = Order::create([...]);
|
|
31
|
+
foreach ($request->items as $item) { /* create OrderItem, deduct stock */ }
|
|
32
|
+
|
|
33
|
+
Mail::to($request->user())->send(new OrderConfirmation($order));
|
|
34
|
+
return OrderResource::make($order);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct:**
|
|
39
|
+
|
|
40
|
+
```php
|
|
41
|
+
// Service contains all business logic
|
|
42
|
+
class OrderService
|
|
43
|
+
{
|
|
44
|
+
public function __construct(
|
|
45
|
+
private readonly InventoryService $inventory,
|
|
46
|
+
private readonly PricingService $pricing,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
public function placeOrder(User $user, array $items, Address $address): Order
|
|
50
|
+
{
|
|
51
|
+
$this->inventory->reserveItems($items); // throws on insufficient stock
|
|
52
|
+
$total = $this->pricing->calculate($items, $user, $address);
|
|
53
|
+
|
|
54
|
+
return DB::transaction(function () use ($user, $items, $total) {
|
|
55
|
+
$order = Order::create(['user_id' => $user->id, 'total' => $total]);
|
|
56
|
+
$this->inventory->commitReservation($order, $items);
|
|
57
|
+
SendOrderConfirmation::dispatch($order);
|
|
58
|
+
return $order;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Controller is just HTTP glue
|
|
64
|
+
class OrderController
|
|
65
|
+
{
|
|
66
|
+
public function __construct(private readonly OrderService $orderService) {}
|
|
67
|
+
|
|
68
|
+
public function store(StoreOrderRequest $request): JsonResponse
|
|
69
|
+
{
|
|
70
|
+
$order = $this->orderService->placeOrder(
|
|
71
|
+
$request->user(),
|
|
72
|
+
$request->validated('items'),
|
|
73
|
+
Address::fromRequest($request),
|
|
74
|
+
);
|
|
75
|
+
return OrderResource::make($order)->response()->setStatusCode(201);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use RefreshDatabase + Factories in Tests — No Manual Inserts
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: manual DB inserts create brittle tests tightly coupled to schema; factories stay in sync with model changes
|
|
5
|
+
tags: testing, factories, refresh-database, pest, phpunit, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use RefreshDatabase + Factories in Tests
|
|
9
|
+
|
|
10
|
+
Hardcoded `DB::table()->insert()` or `User::create([...])` with raw data creates brittle tests. Use model factories with `RefreshDatabase` for isolated, maintainable tests.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
class OrderTest extends TestCase
|
|
16
|
+
{
|
|
17
|
+
protected function setUp(): void
|
|
18
|
+
{
|
|
19
|
+
parent::setUp();
|
|
20
|
+
DB::table('users')->insert(['id' => 1, 'name' => 'Test', 'email' => 'test@test.com', 'password' => 'xxx']);
|
|
21
|
+
DB::table('products')->insert(['id' => 1, 'name' => 'Widget', 'price' => 100, 'stock' => 10]);
|
|
22
|
+
// Test fails when schema changes
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function test_can_place_order()
|
|
26
|
+
{
|
|
27
|
+
$response = $this->post('/orders', ['product_id' => 1, 'qty' => 2]);
|
|
28
|
+
$this->assertEquals(200, $response->getStatusCode());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (PHPUnit):**
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
class OrderTest extends TestCase
|
|
37
|
+
{
|
|
38
|
+
use RefreshDatabase; // wraps each test in a transaction, rolled back after
|
|
39
|
+
|
|
40
|
+
public function test_authenticated_user_can_place_order(): void
|
|
41
|
+
{
|
|
42
|
+
$user = User::factory()->create();
|
|
43
|
+
$product = Product::factory()->create(['price' => 100, 'stock' => 10]);
|
|
44
|
+
|
|
45
|
+
$response = $this->actingAs($user)
|
|
46
|
+
->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 2])
|
|
47
|
+
->assertCreated()
|
|
48
|
+
->assertJsonPath('data.total', 200);
|
|
49
|
+
|
|
50
|
+
$this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 200]);
|
|
51
|
+
$this->assertDatabaseHas('products', ['id' => $product->id, 'stock' => 8]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function test_order_fails_when_insufficient_stock(): void
|
|
55
|
+
{
|
|
56
|
+
$user = User::factory()->create();
|
|
57
|
+
$product = Product::factory()->create(['stock' => 1]);
|
|
58
|
+
|
|
59
|
+
$this->actingAs($user)
|
|
60
|
+
->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 5])
|
|
61
|
+
->assertUnprocessable()
|
|
62
|
+
->assertJsonValidationErrors(['qty']);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Factory patterns:**
|
|
68
|
+
```php
|
|
69
|
+
// Specific state
|
|
70
|
+
$admin = User::factory()->admin()->create();
|
|
71
|
+
$banned = User::factory()->banned()->unverified()->create();
|
|
72
|
+
|
|
73
|
+
// With relations
|
|
74
|
+
$post = Post::factory()->for($user)->hasComments(3)->create();
|
|
75
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Register Services in AppServiceProvider — Never new() in Controllers
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: direct instantiation prevents mocking in tests and creates hidden dependencies
|
|
5
|
+
tags: service-container, dependency-injection, ioc, laravel, testing
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Register Services via Service Container — Never new() in Controllers
|
|
9
|
+
|
|
10
|
+
Laravel's IoC container resolves dependencies automatically via constructor injection. Using `new MyService()` inside a controller or service bypasses the container, making tests hard and coupling impossible to break.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
class OrderController extends Controller
|
|
16
|
+
{
|
|
17
|
+
public function store(StoreOrderRequest $request)
|
|
18
|
+
{
|
|
19
|
+
// Hidden dependencies, impossible to mock
|
|
20
|
+
$service = new OrderService(new InventoryService(), new PricingService());
|
|
21
|
+
$mailer = new OrderMailer(new SmtpTransport());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct:**
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
// 1. Bind in AppServiceProvider if needed (often not needed — auto-resolved)
|
|
30
|
+
class AppServiceProvider extends ServiceProvider
|
|
31
|
+
{
|
|
32
|
+
public function register(): void
|
|
33
|
+
{
|
|
34
|
+
$this->app->singleton(PaymentGateway::class, function () {
|
|
35
|
+
return new StripeGateway(config('services.stripe.secret'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Interface → concrete binding
|
|
39
|
+
$this->app->bind(NotifierInterface::class, SlackNotifier::class);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Controller receives via constructor — auto-resolved by container
|
|
44
|
+
class OrderController extends Controller
|
|
45
|
+
{
|
|
46
|
+
public function __construct(
|
|
47
|
+
private readonly OrderService $orderService,
|
|
48
|
+
private readonly NotifierInterface $notifier,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
public function store(StoreOrderRequest $request): JsonResponse
|
|
52
|
+
{
|
|
53
|
+
$order = $this->orderService->placeOrder($request->user(), $request->validated());
|
|
54
|
+
$this->notifier->notify("New order #{$order->id}");
|
|
55
|
+
return OrderResource::make($order)->response()->setStatusCode(201);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. In tests — swap the binding
|
|
60
|
+
$this->app->instance(NotifierInterface::class, $mockNotifier);
|
|
61
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Use Mutable Default Argument
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Mutable default arguments are shared across all calls, causing subtle state bugs that are very hard to trace.
|
|
5
|
+
tags: python, bugs, arguments, pitfalls, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Use Mutable Default Argument
|
|
9
|
+
|
|
10
|
+
In Python, default argument values are evaluated **once** at function definition time, not at each call. Using mutable objects (list, dict, set) as defaults causes all callers to share the same object, leading to unexpected side effects across calls.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
```python
|
|
14
|
+
def add_item(item, collection=[]):
|
|
15
|
+
collection.append(item)
|
|
16
|
+
return collection
|
|
17
|
+
|
|
18
|
+
print(add_item("a")) # ["a"]
|
|
19
|
+
print(add_item("b")) # ["a", "b"] ← bug: list persists between calls
|
|
20
|
+
|
|
21
|
+
def create_user(name, roles={"admin": False}):
|
|
22
|
+
roles["user"] = True
|
|
23
|
+
return {"name": name, "roles": roles}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct:**
|
|
27
|
+
```python
|
|
28
|
+
def add_item(item, collection=None):
|
|
29
|
+
if collection is None:
|
|
30
|
+
collection = []
|
|
31
|
+
collection.append(item)
|
|
32
|
+
return collection
|
|
33
|
+
|
|
34
|
+
print(add_item("a")) # ["a"]
|
|
35
|
+
print(add_item("b")) # ["b"] ← each call gets a fresh list
|
|
36
|
+
|
|
37
|
+
def create_user(name, roles=None):
|
|
38
|
+
if roles is None:
|
|
39
|
+
roles = {"admin": False}
|
|
40
|
+
roles["user"] = True
|
|
41
|
+
return {"name": name, "roles": roles}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Dataclass alternative (Python 3.7+):**
|
|
45
|
+
```python
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from typing import List
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Task:
|
|
51
|
+
name: str
|
|
52
|
+
tags: List[str] = field(default_factory=list) # safe: fresh list per instance
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Tools:** Ruff `B006` (mutable-argument-default), Pylint `W0102` (dangerous-default-value), flake8-bugbear, mypy
|
|
@@ -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
|