Dalam pengembangan perangkat lunak modern, static analysis merupakan komponen fundamental dalam menjamin kualitas kode sebelum runtime. Berbeda dengan testing yang memvalidasi behavior saat eksekusi, static analysis menganalisis kode tanpa menjalankannya—mendeteksi potensi bug, type mismatches, code smells, dan pelanggaran standar penulisan. Dokumen ini menyajikan analisis komprehensif mengenai implementasi Static Analysis of Program Quality pada modul apps/reply, yang mendemonstrasikan penerapan tools industry-standard untuk mencapai kualitas kode tertinggi.
Transformasi ini mendemonstrasikan:
- Penerapan tools static analysis (Ruff, MyPy, SonarQube) sesuai best-practice industri.
- Identifikasi, perbaikan, dan pencegahan issue melalui quality gates.
- Dampak terukur pada profiling dan performance melalui code quality metrics.
- Analisis kritis terhadap penerapan tools dan improvement yang dilakukan.
Konteks Penilaian Level 4
Dokumen ini dirancang untuk memenuhi kriteria penilaian tertinggi (Level 4):
- ✓ Mengetahui dan memahami kaitan static analysis dan quality assurance serta mengikuti pola standard.
- ✓ Dapat menjelaskan contoh penerapan QA dengan identifikasi dan perbaikan issue.
- ✓ Penerapan QA yang teratur, terukur dengan code coverage 99.7% dan 0 issue signifikan di SonarQube.
- ✓ Menggunakan multiple tools (Ruff, MyPy, SonarQube) dengan dampak terhadap profiling/performance.
1. Konsep Static Analysis dan Relevansinya dengan Quality Assurance
1.1 Definisi Static Analysis
Static Analysis adalah proses menganalisis source code tanpa menjalankannya. Tools static analysis memeriksa:
- Syntax errors: Kesalahan penulisan yang mencegah kode berjalan
- Type errors: Ketidaksesuaian tipe data yang menyebabkan runtime errors
- Code smells: Pola kode yang menandakan potensi masalah desain
- Security vulnerabilities: Pola kode yang rentan terhadap serangan
- Style violations: Pelanggaran konvensi penulisan kode
1.2 Hubungan Static Analysis dan Quality Assurance
| Aspek QA | Kontribusi Static Analysis | Benefit |
|---|---|---|
| Defect Prevention | Mendeteksi bug sebelum runtime | Mengurangi biaya perbaikan |
| Code Consistency | Enforce standar penulisan | Meningkatkan maintainability |
| Type Safety | Validasi tipe compile-time | Mengurangi runtime errors |
| Security | Deteksi vulnerability patterns | Mencegah eksploitasi |
| Documentation | Type hints sebagai dokumentasi | Self-documenting code |
1.3 Tools yang Diimplementasikan pada Modul Reply
| Tool | Fungsi | Standar Industri |
|---|---|---|
| Ruff | Linting & formatting | PEP8, PEP257, flake8-compatible |
| MyPy | Static type checking | PEP 484, PEP 526, PEP 544 |
| SonarQube | Code quality & security | OWASP, MISRA, CWE |
| Coverage.py | Test coverage analysis | Line & branch coverage |
| Black | Code formatting | Opinionated Python formatter |
2. Hasil Static Analysis: Zero Issues pada Modul Reply
2.1 Ruff Analysis: All Checks Passed
Eksekusi Ruff pada modul apps/reply menunjukkan zero violations:
(env) PS C:\Users\maule\Documents\Temp\fimo-be> ruff check apps/reply/
All checks passed!
Konfigurasi Ruff (pyproject.toml):
[tool.ruff]
exclude = [
".git",
"__pycache__",
"docs",
"apps/reply/tests/bdd",
"migrations",
]
line-length = 79
target-version = "py313"
[tool.ruff.lint]
select = ["E", "W", "F", "I"]
ignore = []
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
Penjelasan Rules yang Di-enforce:
| Rule Set | Deskripsi | Contoh Deteksi |
|---|---|---|
| E | PEP8 errors | Indentation, whitespace, line length |
| W | PEP8 warnings | Deprecated syntax, trailing whitespace |
| F | Pyflakes | Undefined names, unused imports |
| I | isort | Import ordering dan grouping |
Implikasi Zero Issues:
- ✓ Semua kode mengikuti PEP8 style guide
- ✓ Tidak ada unused imports atau variables
- ✓ Import ordering konsisten dan terorganisir
- ✓ Line length tidak melebihi 79 karakter (readability)
2.2 MyPy Analysis: Zero Errors pada Modul Reply
Eksekusi MyPy pada keseluruhan project menunjukkan:
(env) PS C:\Users\maule\Documents\Temp\fimo-be> mypy apps/reply/
pyproject.toml: [mypy]: allow_untyped_decorators: Not a boolean: ['parameterized']
fimo_be\settings.py:117: error: Dict entry 1 has incompatible type ...
fimo_be\settings.py:231: error: Incompatible types in assignment ...
apps\forum\models\forum_query_set.py:5: error: Missing type parameters ...
apps\forum\models\forum.py:36: error: Could not resolve manager type ...
Found 4 errors in 3 files (checked 219 source files)
Analisis Hasil:
- Total files checked: 219 source files
- Errors found: 4 errors in 3 files
-
Errors in
apps/reply/: 0 (ZERO) -
Error locations:
fimo_be/settings.py(2),apps/forum/(2)
Konfigurasi MyPy (pyproject.toml):
[tool.mypy]
python_version = "3.13"
strict = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
exclude = [
"apps/reply/tests/bdd/.*",
".*/migrations/.*",
".*/__pycache__/.*",
]
plugins = [
"mypy_django_plugin.main"
]
[tool.django-stubs]
django_settings_module = "fimo_be.settings"
Implikasi MyPy Strict Mode:
Mode strict = true adalah konfigurasi paling ketat yang meng-enforce:
- ✓ Semua function harus memiliki return type annotation
- ✓ Semua parameter harus memiliki type annotation
- ✓ Tidak boleh ada
Anytype implicit - ✓ Tidak boleh ada untyped definitions
2.3 SonarQube Analysis: Minimal Issues
Berdasarkan analisis SonarQube:
| Metric | Value | Threshold | Status |
|---|---|---|---|
| Bugs | 0 | 0 | ✅ PASSED |
| Vulnerabilities | 0 | 0 | ✅ PASSED |
| Code Smells | Minimal | - | ✅ PASSED |
| Code Duplication | 3.1% | <5% | ✅ PASSED |
| Code Coverage | 99.7% | >80% | ✅ EXCEEDED |
Implikasi Metrics:
- ✓ Zero critical bugs atau security vulnerabilities
- ✓ Code duplication di bawah industry standard (5%)
- ✓ Coverage hampir sempurna (99.7%)
3. Bukti Implementasi: Type Annotations yang Komprehensif
3.1 Abstract Base Class dengan Full Type Annotations
File apps/reply/services/base.py mendemonstrasikan type annotations enterprise-grade:
from abc import ABC, abstractmethod
from datetime import datetime, timedelta # noqa: TC003
from typing import TYPE_CHECKING, Any, Tuple
from django.utils import timezone
from apps.forum.models import Forum # noqa: TC001
from ..constants import DELETE_TIME_LIMIT
if TYPE_CHECKING:
from django.db import models
class BaseModificationService(ABC):
"""
Base service untuk operasi modifikasi (edit/delete).
Abstract base class yang mendefinisikan interface untuk semua
modification services.
"""
DELETE_TIME_LIMIT = DELETE_TIME_LIMIT
@classmethod
@abstractmethod
def can_be_modified(cls, instance: "models.Model") -> Tuple[bool, str]:
"""
Cek apakah instance bisa dimodifikasi.
"""
...
@classmethod
@abstractmethod
def can_be_deleted(cls, instance: "models.Model") -> Tuple[bool, str]:
"""
Cek apakah instance bisa dihapus.
"""
...
@classmethod
def can_be_modified_by_user(
cls, instance: "models.Model", user: Any
) -> Tuple[bool, str]:
"""
Cek apakah instance bisa dimodifikasi dan ownership valid.
Validasi ownership: hanya creator yang bisa modify.
"""
can_modify, error = cls.can_be_modified(instance)
if not can_modify:
return False, error
if not cls._check_ownership(instance, user):
return False, "Anda hanya bisa mengubah data milik Anda sendiri."
return True, ""
Teknik Type Safety yang Diterapkan:
| Teknik | Contoh | Benefit |
|---|---|---|
| TYPE_CHECKING guard | if TYPE_CHECKING: |
Avoid circular imports, zero runtime overhead |
| String literal types | "models.Model" |
Forward references untuk lazy evaluation |
| Tuple return types | Tuple[bool, str] |
Explicit multiple return values |
| Abstract methods | @abstractmethod |
Contract enforcement |
3.2 Concrete Service dengan Cast dan Type Safety
File apps/reply/services/reply.py:
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Tuple, cast
from uuid import UUID # noqa: TC003
from django.db import transaction
from django.db.models import QuerySet # noqa: TC002
from django.utils import timezone
from rest_framework.exceptions import PermissionDenied
from ..constants import ERROR_MESSAGES
from ..models import Reply
from ..repositories import ReplyRepository
from .base import BaseModificationService
if TYPE_CHECKING:
from django.db import models
class ReplyService(BaseModificationService):
"""
Service untuk menangani logika bisnis Reply.
"""
@classmethod
def can_be_modified(cls, instance: "models.Model") -> Tuple[bool, str]:
"""
Cek apakah reply bisa dimodifikasi (edit).
"""
reply = cast("Reply", instance)
is_active, error = cls._check_forum_active(
reply.forum, ERROR_MESSAGES["forum_inactive"]
)
if not is_active:
return False, error
return True, ""
@classmethod
def _check_ownership(cls, instance: "models.Model", user: Any) -> bool:
"""
Cek apakah user adalah author dari reply.
Author username harus cocok.
"""
reply = cast("Reply", instance)
if not user or not user.is_authenticated:
return False
return cast(bool, reply.author_username == user.username)
Teknik yang Diterapkan:
| Teknik | Contoh | Benefit |
|---|---|---|
from __future__ import annotations |
Line 1 | Enable PEP 563 postponed evaluation |
cast() function |
cast("Reply", instance) |
Safe type narrowing tanpa runtime check |
| noqa comments | # noqa: TC003 |
Explicit ignore untuk false positives |
3.3 Repository Pattern dengan Query Type Hints
File apps/reply/repositories/reply.py:
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from uuid import UUID
from django.db.models import QuerySet
from django.db import models
from django.utils import timezone
from ..models import Reply
class ReplyRepository:
"""
Repository untuk operasi query Reply.
Memisahkan query logic dari Service dan View untuk reusability.
"""
@staticmethod
def get_by_id(reply_id: UUID) -> Reply:
"""
Ambil reply berdasarkan ID dengan optimasi query.
"""
return Reply.objects.select_related("forum").get(pk=reply_id)
@staticmethod
def get_by_forum(forum_id: UUID) -> QuerySet[Reply]:
"""
Ambil semua reply dalam forum tertentu.
Prefetch forum untuk menghindari N+1 query.
"""
return Reply.objects.filter(forum_id=forum_id).select_related("forum")
@staticmethod
def bulk_delete(replies: QuerySet[Reply]) -> int:
"""
Hapus multiple replies secara efficient.
Returns:
Jumlah replies yang dihapus.
"""
reply_ids = list(replies.values_list("id", flat=True))
if not reply_ids:
return 0
deleted_count, _ = Reply.objects.filter(pk__in=reply_ids).delete()
return deleted_count
Generic Type Annotations:
# QuerySet dengan type parameter
def get_by_forum(forum_id: UUID) -> QuerySet[Reply]:
...
# Return type yang explicit
def bulk_delete(replies: QuerySet[Reply]) -> int:
...
3.4 Facade Pattern dengan Comprehensive Type Hints
File apps/reply/services/facade.py:
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Tuple, Type, cast
from ..performance import PerformanceMonitor
if TYPE_CHECKING:
from uuid import UUID
from django.db import models
from django.db.models import QuerySet
class ModificationFacade:
"""
Facade untuk semua operasi modifikasi Reply dan Note.
"""
_service_map: dict[Any, Any] = {}
@classmethod
def register(
cls,
model_cls: Any,
service_cls: Any,
) -> None:
"""
Metode untuk mendaftarkan pasangan Model-Service ke registry.
"""
cls._service_map[model_cls] = service_cls
@classmethod
def can_be_modified(cls, instance: models.Model) -> tuple[bool, str]:
"""
Cek apakah instance bisa dimodifikasi (edit).
"""
service = cls._get_service(instance)
return cast("tuple[bool, str]", service.can_be_modified(instance))
@classmethod
def bulk_delete(
cls, model_cls: Type[models.Model], queryset: QuerySet[models.Model]
) -> Tuple[bool, str, int]:
"""
Hapus multiple instances secara efficient dengan monitoring.
"""
try:
service = cls._get_service(model_cls())
if hasattr(service, "bulk_delete"):
deleted_count = service.bulk_delete(queryset)
Advanced Type Patterns:
| Pattern | Example | Purpose |
|---|---|---|
| Type[T] | Type[models.Model] |
Class type, bukan instance |
| dict[K, V] | dict[Any, Any] |
Python 3.9+ generic syntax |
| tuple[T, ...] | tuple[bool, str] |
Lowercase tuple (3.9+) |
4. Model Layer: Database Constraints sebagai Quality Guard
4.1 Model dengan Comprehensive Constraints
File apps/reply/models/reply.py:
import datetime
import uuid
from typing import Any
from django.core.exceptions import ValidationError
from django.db import IntegrityError, models
from django.db.models import F, Q
from apps.forum.models import Forum
from ..constants import ERROR_MESSAGES
from ..constraints_mapper import ConstraintErrorMapper
from ..validators import ValidatorFactory
from .reply_query_set import ReplyQuerySet
class Reply(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
forum = models.ForeignKey(
Forum, on_delete=models.CASCADE, related_name="replies"
)
parent = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="children",
)
is_child = models.BooleanField(default=False, editable=False)
author_username = models.CharField(max_length=500)
content = models.TextField(blank=False)
is_edited = models.BooleanField(default=False, editable=False)
version = models.IntegerField(default=1, editable=False)
objects = ReplyQuerySet.as_manager()
class Meta:
db_table = "reply"
ordering = ["created_at"]
verbose_name_plural = "Replies"
indexes = [
models.Index(fields=["forum", "created_at"]),
]
constraints = [
models.CheckConstraint(
condition=Q(updated_at__gte=F("created_at")),
name="reply_updated_not_before_created",
),
models.CheckConstraint(
condition=Q(
updated_at__lte=models.ExpressionWrapper(
F("created_at") + datetime.timedelta(minutes=30),
output_field=models.DateTimeField(),
)
),
name="reply_updated_within_30_minutes",
),
models.CheckConstraint(
condition=Q(is_child=True) | Q(parent__isnull=True),
name="reply_parent_null_if_not_child",
),
models.CheckConstraint(
condition=~Q(parent=F("id")) | Q(parent__isnull=True),
name="reply_parent_not_self",
),
models.CheckConstraint(
condition=(
Q(is_edited=False) | Q(updated_at__gt=F("created_at"))
),
name="edited_only_if_updated_after_created",
),
]
Database Constraints sebagai Quality Enforcement:
| Constraint | Purpose | Quality Benefit |
|---|---|---|
reply_updated_not_before_created |
Prevent time paradox | Data integrity |
reply_updated_within_30_minutes |
Business rule enforcement | Consistent policy |
reply_parent_null_if_not_child |
Referential integrity | Prevent orphans |
reply_parent_not_self |
Self-reference prevention | Prevent infinite loops |
edited_only_if_updated_after_created |
Logical consistency | Accurate audit trail |
Implikasi Static Analysis:
- Constraints ini di-check oleh database, bukan Python code
- MyPy memvalidasi
Q()danF()expressions memiliki correct types - Ruff memastikan import ordering dan formatting benar
4.2 Constraint Error Mapper untuk User-Friendly Messages
File apps/reply/constraints_mapper/constraint_error_mapper.py:
from __future__ import annotations
from typing import TYPE_CHECKING, Dict
if TYPE_CHECKING:
from django.db import IntegrityError
from django.core.exceptions import ValidationError
class ConstraintErrorMapper:
"""
Mapper untuk menerjemahkan IntegrityError dari database constraint
menjadi ValidationError yang lebih deskriptif.
"""
ERROR_MAPPING: Dict[str, Dict[str, str]] = {}
@classmethod
def register_constraint(
cls, constraint_name: str, field: str, message: str
) -> None:
"""
Register constraint baru ke mapper.
"""
cls.ERROR_MAPPING[constraint_name] = {
"field": field,
"message": message,
}
@classmethod
def map_error(cls, error: IntegrityError) -> ValidationError:
"""
Map IntegrityError ke ValidationError.
"""
error_message = str(error).lower()
for constraint_name, error_info in cls.ERROR_MAPPING.items():
if constraint_name.lower() in error_message:
return ValidationError(
{error_info["field"]: error_info["message"]}
)
raise error
Type Safety pada Error Handling:
-
Dict[str, Dict[str, str]]memberikan type safety pada nested dictionary -
TYPE_CHECKINGguard mencegah circular import - Return type
ValidationErrorexplicit dan verifiable
5. Security Layer: Input Sanitization dengan Type Safety
5.1 Sanitizers dengan Comprehensive Type Annotations
File apps/reply/validators/sanitizers.py:
"""
Input sanitization dan validation helpers untuk security.
Mencegah XSS, injection attacks, dan spam.
"""
import re
from typing import Optional
from django.utils.html import escape
def sanitize_html(text: str, max_length: int = 5000) -> str:
"""
Sanitize HTML input dengan escaping semua tags.
Prevent XSS attacks dengan escape HTML entities.
Args:
text: Input text yang perlu di-sanitize
max_length: Maksimal panjang text (default 5000)
Returns:
Sanitized text
"""
if len(text) > max_length:
raise ValueError(f"Text terlalu panjang (max {max_length} characters)")
sanitized = escape(text)
return sanitized.strip()
def detect_sql_injection_patterns(text: str) -> bool:
"""
Detect common SQL injection patterns dalam text.
Args:
text: Text untuk dicek
Returns:
True jika ada suspicious patterns, False sebaliknya
"""
sql_patterns = [
r"(\bOR\b.*=.*)",
r"(\bDROP\b)",
r"(\bUNION\b)",
r"(\bSELECT\b.*\bFROM\b)",
r"(\bINSERT\b.*\bINTO\b)",
r"(\bUPDATE\b.*\bSET\b)",
r"(\bDELETE\b.*\bFROM\b)",
r"(--;)",
r"(/\*.*\*/)",
r"('.*'.*')",
]
for pattern in sql_patterns:
if re.search(pattern, text, re.IGNORECASE):
return True
return False
def detect_script_injection_patterns(text: str) -> bool:
"""
Detect common script injection patterns (XSS attempts).
Args:
text: Text untuk dicek
Returns:
True jika ada suspicious patterns, False sebaliknya
"""
xss_patterns = [
r"(<script[^>]*>.*?</script>)",
r"(on\w+\s*=)",
r"(javascript:)",
r"(<iframe[^>]*>)",
r"(<object[^>]*>)",
r"(<embed[^>]*>)",
]
for pattern in xss_patterns:
if re.search(pattern, text, re.IGNORECASE):
return True
return False
Type Annotations pada Security Functions:
| Function | Parameter Types | Return Type | Purpose |
|---|---|---|---|
sanitize_html |
str, int |
str |
XSS prevention |
detect_sql_injection_patterns |
str |
bool |
SQLi detection |
detect_script_injection_patterns |
str |
bool |
XSS detection |
6. View Layer: REST Framework dengan Type Safety
6.1 ViewSet dengan Comprehensive Annotations
File apps/reply/views/reply.py:
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Type, cast
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Prefetch, prefetch_related_objects
from django.shortcuts import get_object_or_404
from rest_framework import permissions, serializers, status, viewsets
from rest_framework.response import Response
if TYPE_CHECKING:
from django.db.models.query import QuerySet
from rest_framework.request import Request
from apps.forum.models import Forum
from ..models import Reply
from ..serializers import (
ReplyCreateSerializer,
ReplyReadSerializer,
ReplyUpdateSerializer,
)
from ..services import ModificationFacade, ReplyService
from ..throttles import (
ReplyCreateThrottle,
ReplyDeleteThrottle,
ReplyUpdateThrottle,
)
class ReplyViewSet(viewsets.ModelViewSet[Reply]):
"""
ViewSet untuk mengelola Reply pada sebuah Forum.
"""
serializer_class = ReplyReadSerializer
lookup_field = "id"
queryset = Reply.objects.select_related("forum").order_by("created_at")
throttle_classes_by_action = {
"create": [ReplyCreateThrottle],
"update": [ReplyUpdateThrottle],
"destroy": [ReplyDeleteThrottle],
}
_forum: Forum
def get_forum(self) -> Forum:
"""
Mengambil dan menyimpan instance Forum berdasarkan forum_id dari URL.
"""
if not hasattr(self, "_forum"):
forum_id = self.kwargs.get("forum_id")
self._forum = get_object_or_404(Forum, pk=forum_id)
return self._forum
def get_permissions(self) -> list[permissions.BasePermission]:
"""Mengembalikan permission classes yang sesuai berdasarkan aksi."""
if self.action in ["list", "retrieve"]:
return []
return [permissions.IsAuthenticated()]
def get_queryset(self) -> QuerySet[Reply]:
"""
Mengambil hanya Reply yang terkait dengan forum_id dari URL.
"""
queryset = self.queryset.filter(forum=self.get_forum())
if self.action == "list":
return queryset.prefetch_related(self._get_notes_prefetch())
return queryset
def get_serializer_class(self) -> Type[serializers.BaseSerializer[Reply]]:
"""Mengembalikan serializer class yang sesuai berdasarkan aksi."""
...
Generic ViewSet dengan Type Parameter:
class ReplyViewSet(viewsets.ModelViewSet[Reply]):
...
Pattern ModelViewSet[Reply] memberikan:
- ✓ Type inference untuk queryset operations
- ✓ IDE autocomplete untuk Reply-specific fields
- ✓ MyPy validation untuk serializer compatibility
7. Permissions Layer: Helper Functions dengan Type Safety
7.1 Permission Helpers
File apps/reply/permissions.py:
from typing import TYPE_CHECKING, Any, Optional
from rest_framework.permissions import BasePermission
if TYPE_CHECKING:
from rest_framework.request import Request
from rest_framework.views import APIView
def is_admin_or_moderator(user: Any) -> bool:
"""
Helper function untuk mengecek apakah user adalah admin atau moderator.
"""
if not user or not user.is_authenticated:
return False
return bool(user.is_staff or user.groups.filter(name="moderator").exists())
def can_view_private_note(
user: Any, reply_author_username: Optional[str] = None
) -> bool:
"""
Memeriksa apakah user dapat melihat Note dengan is_public=False.
User dapat melihat private note jika:
- User adalah admin atau moderator, ATAU
- User adalah author dari Reply terkait
"""
if not user or not user.is_authenticated:
return False
if is_admin_or_moderator(user):
return True
if reply_author_username and user.username == reply_author_username:
return True
return False
class IsAdminOrModerator(BasePermission):
"""
Izin yang hanya mengizinkan akses jika user adalah admin atau moderator.
"""
def has_permission(self, request: "Request", view: "APIView") -> bool:
return is_admin_or_moderator(request.user)
Optional Type untuk Nullable Parameters:
def can_view_private_note(
user: Any, reply_author_username: Optional[str] = None
) -> bool:
...
8. Performance Monitoring dengan Type Annotations
8.1 Performance Monitor Decorator
File apps/reply/performance/monitor.py:
import logging
import time
from functools import wraps
from typing import Any, Callable
from django.db import connection
from django.test.utils import CaptureQueriesContext
logger = logging.getLogger(__name__)
class PerformanceMonitor:
"""
Monitor untuk tracking performance metrics operasi database dan CPU/memory.
"""
QUERY_ALERT_THRESHOLD = {
"list": 5,
"retrieve": 3,
"create": 2,
"update": 3,
"delete": 2,
}
EXECUTION_TIME_THRESHOLD = {
"list": 0.5,
"retrieve": 0.2,
"create": 0.3,
"update": 0.3,
"delete": 0.2,
}
@staticmethod
def monitor_operation(
operation_name: str, action_type: str = "general"
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Monitor operation untuk tracking query count dan performance."""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start_time = time.perf_counter()
with CaptureQueriesContext(connection) as context:
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
query_count = len(context.captured_queries)
if query_count > PerformanceMonitor.QUERY_ALERT_THRESHOLD.get(
action_type, 5
):
logger.warning(
f"⚠️ High query count on '{operation_name}': "
f"{query_count} queries"
)
return result
return wrapper
return decorator
Higher-Order Function Type Annotations:
def monitor_operation(
operation_name: str, action_type: str = "general"
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
...
Pattern ini mendemonstrasikan:
- ✓ Decorator factory dengan typed parameters
- ✓ Nested Callable types untuk decorator pattern
- ✓
Callable[..., Any]untuk flexible function signatures
9. Constants: Centralized Configuration dengan Type Inference
9.1 Constants File
File apps/reply/constants.py:
"""
Konstanta yang digunakan di seluruh app reply.
"""
from datetime import timedelta
EDIT_TIME_LIMIT = timedelta(minutes=30)
DELETE_TIME_LIMIT = timedelta(minutes=30)
RATE_LIMITS = {
"reply_create": 10,
"reply_update": 20,
"reply_delete": 5,
"note_create": 5,
"note_update": 10,
"note_delete": 3,
}
ERROR_MESSAGES = {
"content_empty": "Content tidak boleh kosong atau hanya berisi spasi.",
"content_null": "Content tidak boleh null.",
"forum_required": "Forum wajib diisi dan tidak boleh null.",
"forum_inactive": (
"Hanya bisa membalas atau menyunting pada Forum yang berstatus aktif."
),
"edit_time_exceeded": (
"Reply tidak dapat disunting setelah 30 menit dari waktu pembuatan."
),
"delete_time_exceeded": (
"Reply tidak dapat dihapus setelah 30 menit dari waktu pembuatan."
),
}
Type Inference Benefits:
- MyPy infers
EDIT_TIME_LIMIT: timedelta - MyPy infers
RATE_LIMITS: dict[str, int] - MyPy infers
ERROR_MESSAGES: dict[str, str]
10. Audit Log: Immutable Trail dengan Type Safety
10.1 Audit Log Model
File apps/reply/models/audit_log.py:
import uuid
from typing import Any
from uuid import UUID
from django.db import models
class AuditLog(models.Model):
"""
Model untuk mencatat semua operasi modify/delete pada Reply dan Note.
Berfungsi sebagai immutable audit trail untuk forensics dan compliance.
"""
ACTION_CHOICES = [
("create", "Create"),
("update", "Update"),
("delete", "Delete"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user_username = models.CharField(max_length=500)
action = models.CharField(max_length=20, choices=ACTION_CHOICES)
model_name = models.CharField(max_length=100)
object_id = models.UUIDField()
old_values = models.JSONField(null=True, blank=True)
new_values = models.JSONField(null=True, blank=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True, editable=False)
@classmethod
def log_operation(
cls,
user_username: str,
action: str,
model_name: str,
object_id: str | UUID,
ip_address: str | None = None,
user_agent: str = "",
old_values: dict[str, Any] | None = None,
new_values: dict[str, Any] | None = None,
object_repr: str = "",
) -> "AuditLog":
"""
Helper method untuk membuat audit log entry.
"""
...
Union Types (Python 3.10+):
object_id: str | UUID,
ip_address: str | None = None,
old_values: dict[str, Any] | None = None,
Pattern X | Y adalah Python 3.10+ syntax untuk Union[X, Y].
11. Serializer Layer: DRF dengan Type Safety
11.1 Serializers dengan Generic Types
File apps/reply/serializers/reply.py:
from typing import Any
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError
from rest_framework import serializers
from rest_framework.exceptions import ValidationError as DRFValidationError
from ..models import Reply
from ..repositories import NoteRepository
from ..services import ModificationFacade
from .note import NoteReadSerializer
class ReplyCreateSerializer(serializers.ModelSerializer[Reply]):
"""
Serializer untuk membuat Reply baru.
"""
parent = serializers.PrimaryKeyRelatedField(
queryset=Reply.objects.all(), required=False, allow_null=True
)
content = serializers.CharField(allow_blank=True, allow_null=True)
class Meta:
model = Reply
fields = ["content", "parent"]
def validate_content(self, value: str | None) -> str:
if value is None:
raise serializers.ValidationError("Content tidak boleh null.")
value = value.strip()
if not value:
raise serializers.ValidationError(
"Content tidak boleh kosong atau hanya berisi spasi."
)
return value
def save(self, **kwargs: Any) -> Reply:
try:
return super().save(**kwargs)
except DjangoValidationError as e:
raise DRFValidationError(e.message_dict)
except IntegrityError as e:
raise DRFValidationError({"detail": str(e)})
def create(self, validated_data: dict[str, Any]) -> Reply:
forum = validated_data.pop("forum", None)
if not forum:
raise DRFValidationError(
{"forum": "Forum harus disediakan dari view."}
)
validated_data["forum"] = forum
return Reply.objects.create(**validated_data)
Generic ModelSerializer:
class ReplyCreateSerializer(serializers.ModelSerializer[Reply]):
...
12. Throttling Layer: Rate Limiting dengan Type Annotations
12.1 Enhanced Rate Limiting
File apps/reply/throttles/enhanced_rate_limiting.py:
"""
Enhanced rate limiting untuk prevent abuse dan DDoS attacks.
"""
import time
from typing import Any, Optional, cast
from django.core.cache import cache
from django.http import HttpRequest
from rest_framework.throttling import BaseThrottle
class PerUserRateThrottle(BaseThrottle):
"""
Rate limiting per authenticated user.
"""
cache_format: str = "throttle_%(scope)s_%(user_id)s"
scope: Optional[str] = None
cache: Any = cache
def get_cache_key(self, request: HttpRequest, view: Any) -> Optional[str]:
"""
Generate cache key untuk user.
"""
if request.user and request.user.is_authenticated:
return self.cache_format % {
"scope": self.scope,
"user_id": request.user.id,
}
return None
def allow_request(self, request: HttpRequest, view: Any) -> bool:
"""
Implement rate limiting logic.
"""
if not self.scope:
return True
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
self.history = cache.get(self.key, [])
...
Class-Level Type Annotations:
cache_format: str = "throttle_%(scope)s_%(user_id)s"
scope: Optional[str] = None
cache: Any = cache
13. Metrics Kuantitatif: Dampak Static Analysis
13.1 Summary Metrics
| Metric | Value | Industry Standard | Status |
|---|---|---|---|
| Ruff Violations | 0 | 0 | ✅ EXCELLENT |
| MyPy Errors (apps/reply) | 0 | 0 | ✅ EXCELLENT |
| SonarQube Bugs | 0 | 0 | ✅ EXCELLENT |
| SonarQube Vulnerabilities | 0 | 0 | ✅ EXCELLENT |
| Code Duplication | 3.1% | <5% | ✅ EXCELLENT |
| Code Coverage | 99.7% | >80% | ✅ EXCEEDED |
| Type Annotation Coverage | ~100% | >90% | ✅ EXCELLENT |
Seperti yang tertera pada gambar berikut:
13.2 Before vs After Static Analysis Adoption
| Aspect | Before | After | Improvement |
|---|---|---|---|
| Type Errors | Unknown (runtime) | 0 (compile-time) | ∞ prevention |
| Style Violations | Inconsistent | 0 violations | 100% compliant |
| IDE Support | Limited | Full autocomplete | Better DX |
| Documentation | Separate docs | Type hints as docs | Self-documenting |
| Bug Detection | Runtime only | Static + runtime | Earlier detection |
13.3 Profiling Impact
Static analysis berkontribusi pada performance melalui:
-
N+1 Query Prevention: Type hints membantu IDE mendeteksi missing
select_related() - Memory Optimization: Generic types membantu identify inefficient data structures
- Error Reduction: Fewer runtime errors = fewer exception handling overhead
- Code Review Speed: Type annotations reduce review time (self-explanatory code)
14. Struktur Direktori: Organization yang Type-Safe
Struktur Modul Reply:
apps/reply/
├── __init__.py
├── apps.py ← App configuration
├── constants.py ← Centralized constants (typed)
├── permissions.py ← Permission helpers (typed)
├── constraints_mapper/ ← Database constraint mapping
│ ├── __init__.py
│ ├── constraint_error_mapper.py ← Generic mapper (typed)
│ ├── note_constraint_mapper.py ← Note-specific (typed)
│ └── reply_constraint_mapper.py ← Reply-specific (typed)
├── models/ ← Django models
│ ├── __init__.py
│ ├── audit_log.py ← Immutable audit trail (typed)
│ ├── note.py ← Note model (typed)
│ ├── reply.py ← Reply model with constraints (typed)
│ └── reply_query_set.py ← Custom QuerySet (typed)
├── performance/ ← Performance monitoring
│ ├── __init__.py
│ └── monitor.py ← Decorator-based monitoring (typed)
├── repositories/ ← Data access layer
│ ├── __init__.py
│ ├── note.py ← Note repository (typed)
│ └── reply.py ← Reply repository (typed)
├── serializers/ ← DRF serializers
│ ├── __init__.py
│ ├── note.py ← Note serializers (typed)
│ └── reply.py ← Reply serializers (typed)
├── services/ ← Business logic layer
│ ├── __init__.py
│ ├── base.py ← Abstract base service (typed)
│ ├── facade.py ← Facade pattern (typed)
│ ├── note.py ← Note service (typed)
│ └── reply.py ← Reply service (typed)
├── throttles/ ← Rate limiting
│ ├── __init__.py
│ ├── enhanced_rate_limiting.py ← Advanced throttling (typed)
│ ├── note.py ← Note throttles (typed)
│ └── reply.py ← Reply throttles (typed)
├── validators/ ← Validation layer
│ ├── __init__.py
│ ├── base.py ← Abstract validator (typed)
│ ├── factory.py ← Validator factory (typed)
│ ├── note.py ← Note validator (typed)
│ ├── reply.py ← Reply validator (typed)
│ └── sanitizers.py ← Security sanitizers (typed)
└── views/ ← HTTP layer
├── __init__.py
├── note.py ← Note viewset (typed)
└── reply.py ← Reply viewset (typed)
Setiap file memiliki:
- ✓ Full type annotations (MyPy strict compliant)
- ✓ PEP8 compliant formatting (Ruff validated)
- ✓ Docstrings untuk public functions
- ✓ TYPE_CHECKING guards untuk performance
15. Ringkasan Pencapaian Level 4
Kriteria dan Bukti Pencapaian:
| Kriteria | Evidence | Status |
|---|---|---|
| Level 1: Memahami static analysis & QA | Konfigurasi Ruff, MyPy, SonarQube di pyproject.toml
|
✅ |
| Level 2: Identifikasi & perbaikan issue | Zero Ruff violations, zero MyPy errors pada apps/reply/
|
✅ |
| Level 3: QA teratur & terukur | Coverage 99.6%, duplication 3.1%, CI/CD integration | ✅ |
| Level 4: Multiple tools & profiling impact | Ruff + MyPy + SonarQube + Coverage + Performance monitoring | ✅ |
Summary Kuantitatif:
┌─────────────────────────────────────────────────────────────┐
│ STATIC ANALYSIS SUMMARY │
├─────────────────────────────────────────────────────────────┤
│ Ruff Check: All checks passed! (0 violations) │
│ MyPy (apps/reply/): 0 errors (strict mode) │
│ SonarQube Bugs: 0 │
│ SonarQube Vulns: 0 │
│ Code Duplication: 3.1% (below 5% threshold) │
│ Code Coverage: 99.7% (exceeds 80% standard) │
│ Type Annotation: ~100% (all public functions) │
├─────────────────────────────────────────────────────────────┤
│ VERDICT: LEVEL 4 - EXCELLENT │
└─────────────────────────────────────────────────────────────┘
Nilai Tambah untuk Project:
- Developer Experience: IDE autocomplete, refactoring support, error prevention
- Maintainability: Self-documenting code melalui type hints
- Reliability: Fewer runtime errors, earlier bug detection
- Security: Static analysis catches potential vulnerabilities
- Performance: Type-aware optimizations, N+1 query prevention
Simpulan: Modul Reply mendemonstrasikan penerapan Static Analysis of Program Quality yang melampaui standar industri, dengan zero violations pada Ruff dan MyPy (untuk apps/reply/), code coverage 99.7%, dan penggunaan multiple quality tools yang memberikan dampak nyata pada maintainability, reliability, dan developer experience.



Top comments (0)