Back to blog
May 03, 2023
8 min read

Ensuring your Program Quality with SonarQube

How to use SonarQube to improve your code quality.

Selama proses pengembangan aplikasi pada proyek PPL, kami diharapkan untuk mampu menghasilkan program dengan kualitas yang baik. Memastikan code tidak mengandung code smell, memastikan test case meng-cover seluruh behavior program, memastikan tidak terdapat security issue pada program, dan sebagainya menjadi hal yang wajib saat pengembangan aplikasi pada proyek PPL.

Namun, hal tersebut semakin sulit dilakukan seiring dengan bertambah besar dan kompleks aplikasi yang dikembangkan. Oleh karena itu, diperlukan tools pendukung yang dapat membantu kita dalam memastikan kualitas program. Beberapa contoh dari tools tersebut meliputi SonarQube, SonarLint, CodeClimate, dan sebagainya. Pada artikel ini, akan dibahas penerapan SonarQube pada proyek PPL yang saya kerjakan.

SonarQube

SonarQube adalah sebuah platform analisis code yang digunakan untuk meningkatkan kualitas software. Dengan SonarQube, kita dapat menganalisis code yang kita buat dan mendapatkan feedback terkait kualitas dari code yang telah dibuat. SonarQube berisi berbagai fitur yang dapat membantu meningkatkan kualitas kode. Fitur-fitur tersebut meliputi analisis code smell, duplicated code, code coverage, dan security hotspot.

Code Smell

Code smell merupakan salah satu faktor penentu kualitas code. Beberapa contoh code smell meliputi duplicated string, penggunaan print pada code production, nama function yang tidak sesuai konvensi, dan masih banyak lagi. Hal tersebut akan sangat sulit rasanya untuk dianalisis secara manual. SonarQube dapat melakukan analisis pada code dan menentukan apakah code masih mengandung code smell (Kriteria code smell SonarQube). Sebagai contoh, perhatikan code dari proyek PPL yang sedang saya kerjakan

@route.get('/list', response=List[LoanProductSchema], auth=JWTAuth())
    @paginate()
    def get_loan_product_list(
            self,
            product_id: str = None,
            product_name: str = None,
            description: str = None,
            category: str = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        if product_id:
            query &= Q(id__icontains=product_id)
        if product_name:
            query &= Q(category__icontains=category)
        try:
            if create_date_start:
                create_date_start = datetime.strptime(create_date_start, '%d-%m-%Y')
                query &= Q(create_date__gte=create_date_start)
            if create_date_end:
                create_date_end = datetime.strptime(create_date_end, '%d-%m-%Y') + timedelta(days=1)
                query &= Q(create_date__lte=create_date_end)
            if update_date_start:
                update_date_start = datetime.strptime(update_date_start, '%d-%m-%Y')
                query &= Q(update_date__gte=update_date_start)
            if update_date_end:
                update_date_end = datetime.strptime(update_date_end, '%d-%m-%Y') + timedelta(days=1)
                query &= Q(update_date__lte=update_date_end)
        except ValueError:
            raise HttpError(400, "Invalid date format. Use DD-MM-YYYY.")

        loan_products = LoanProduct.objects.filter(query)
        return loan_products

Code diatas merupakah implementasi dari salah satu endpoint API pada proyek PPL saya. Sekilas tidak terdapat masalah pada code tersebut. Saya sendiri tidak menyadari adanya code smell sampai dilakukan analisis dengan SonarQube yang ternyata menemukan code smell.

Analisis pada SonarQube menunjukan code smell pada code

Selain menemukan code smell, SonarQube juga dapat memberikan detail bagian code yang meghasilkan code smell dan juga memberikan penjelasan terhadap code smell dan contoh cara menghilangkan code smell tersebut

Detail dan penjelasan code smell

Berdasarkan analisis tersebut terdapat duplicated string pada code, sehingga salah satu cara yang dapat dilakukan untuk menghindari code smell tersebut adalah dengan membuat variable yang merepresentasikan duplicated string tersebut.

@route.get('/list', response=List[LoanProductSchema], auth=JWTAuth())
    @paginate()
    def get_loan_product_list(
            self,
            product_id: str = None,
            product_name: str = None,
            description: str = None,
            category: str = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        dateformat = '%d-%m-%Y'
        if product_id:
            query &= Q(id__icontains=product_id)
        if product_name:
            query &= Q(category__icontains=category)
        try:
            if create_date_start:
                create_date_start = datetime.strptime(create_date_start, dateformat)
                query &= Q(create_date__gte=create_date_start)
            if create_date_end:
                create_date_end = datetime.strptime(create_date_end, dateformat) + timedelta(days=1)
                query &= Q(create_date__lte=create_date_end)
            if update_date_start:
                update_date_start = datetime.strptime(update_date_start, dateformat)
                query &= Q(update_date__gte=update_date_start)
            if update_date_end:
                update_date_end = datetime.strptime(update_date_end, dateformat) + timedelta(days=1)
                query &= Q(update_date__lte=update_date_end)
        except ValueError:
            raise HttpError(400, "Invalid date format. Use DD-MM-YYYY.")

        loan_products = LoanProduct.objects.filter(query)
        return loan_products

Duplicated Code

Duplicated code juga merupakan faktor penentu kualitas program. Program yang memiliki banyak duplicated code cenderung memiliki maintanibility yang lebih buruk. SonarQube dapat mendeteksi duplicated code yang terdapat pada program kita. Sebagai contoh, perhatikan 2 contoh potongan code dari proyek PPL saya

@route.get('/list', response=List[LoanProductSchema], auth=JWTAuth())
    @paginate()
    def get_loan_product_list(
            self,
            product_id: str = None,
            product_name: str = None,
            description: str = None,
            category: str = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        dateformat = '%Y-%m-%d'
        if product_id:
            query &= Q(id__icontains=product_id)
        if product_name:
            query &= Q(name__icontains=product_name)
        if description:
            query &= Q(description__icontains=description)
        if category:
            query &= Q(category__icontains=category)
        try:
            if create_date_start:
                create_date_start = datetime.strptime(create_date_start, dateformat)
                query &= Q(create_date__gte=create_date_start)
            if create_date_end:
                create_date_end = datetime.strptime(create_date_end, dateformat) + timedelta(days=1)
                query &= Q(create_date__lte=create_date_end)
            if update_date_start:
                update_date_start = datetime.strptime(update_date_start, dateformat)
                query &= Q(update_date__gte=update_date_start)
            if update_date_end:
                update_date_end = datetime.strptime(update_date_end, dateformat) + timedelta(days=1)
                query &= Q(update_date__lte=update_date_end)
        except ValueError:
            raise HttpError(400, "Invalid date format. Use YYYY-MM-DD.")

        loan_products = LoanProduct.objects.filter(query).annotate(
            created_date=Cast(TruncMinute("create_date"), CharField()),
            updated_date=Cast(TruncMinute("update_date"), CharField())
        )
        return list(loan_products)
@route.get('/list', response=List[SectionSchema], auth=JWTAuth())
    @paginate()
    def get_section_list(
            self,
            section_id: str = None,
            section_name: str = None,
            section_minimum_score: int = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        dateformat = '%Y-%m-%d'
        if section_id:
            query &= Q(id__icontains=section_id)
        if section_name:
            query &= Q(name__icontains=section_name)
        if section_minimum_score:
            query &= Q(minimum_score=section_minimum_score)
        try:
            if create_date_start:
                create_date_start = datetime.strptime(create_date_start, dateformat)
                query &= Q(create_date__gte=create_date_start)
            if create_date_end:
                create_date_end = datetime.strptime(create_date_end, dateformat) + timedelta(days=1)
                query &= Q(create_date__lte=create_date_end)
            if update_date_start:
                update_date_start = datetime.strptime(update_date_start, dateformat)
                query &= Q(update_date__gte=update_date_start)
            if update_date_end:
                update_date_end = datetime.strptime(update_date_end, dateformat) + timedelta(days=1)
                query &= Q(update_date__lte=update_date_end)
        except ValueError:
            raise HttpError(400, "Invalid date format. Use YYYY-MM-DD.")

        sections = Section.objects.filter(query).annotate(
            created_date=Cast(TruncMinute("create_date"), CharField()),
            updated_date=Cast(TruncMinute("update_date"), CharField()),
            question_count=Count('question')
        )

        return list(sections)

Kedua function tersebut merepresentasikan endpoint API untuk mendapatkan dan melakukan filter pada loan product dan section. Bagian filter berdasarkan tanggal dari kedua function tersebut sama persis (duplicated code). Hal tersebut dapat terdeteksi oleh SonarQube lengkap dengan menunjukan persentase duplicated code pada program serta bagian yang mengandung duplicated code.

Persentase duplicated code hasil deteksi SonarQube

Detail bagian code yang mengandung duplicated code

Hal yang saya lakukan untuk mengatasi duplicated code tersebut adalah dengan melakukan extract bagian filter sebagai function seperti berikut

def get_filtered_objects(model, query, dateformat, **kwargs):
    try:
        if kwargs.get('create_date_start'):
            create_date_start = datetime.strptime(kwargs.pop('create_date_start'), dateformat)
            query &= Q(create_date__gte=create_date_start)
        if kwargs.get('create_date_end'):
            create_date_end = datetime.strptime(kwargs.pop('create_date_end'), dateformat) + timedelta(days=1)
            query &= Q(create_date__lte=create_date_end)
        if kwargs.get('update_date_start'):
            update_date_start = datetime.strptime(kwargs.pop('update_date_start'), dateformat)
            query &= Q(update_date__gte=update_date_start)
        if kwargs.get('update_date_end'):
            update_date_end = datetime.strptime(kwargs.pop('update_date_end'), dateformat) + timedelta(days=1)
            query &= Q(update_date__lte=update_date_end)
    except ValueError:
        raise HttpError(400, "Invalid date format. Use YYYY-MM-DD.")
    for key, value in kwargs.items():
        if value:
            query &= Q(**{key + '__icontains': value})
    objects = model.objects.filter(query).annotate(
        created_date=Cast(TruncMinute("create_date"), CharField()),
        updated_date=Cast(TruncMinute("update_date"), CharField())
    )
    return objects
@route.get('/list', response=List[LoanProductSchema], auth=JWTAuth())
    @paginate()
    def get_loan_product_list(
            self,
            product_id: str = None,
            product_name: str = None,
            description: str = None,
            category: str = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        dateformat = '%Y-%m-%d'
        loanproducts = get_filtered_objects(LoanProduct, query, dateformat, id=product_id, name=product_name,
                                            description=description, category=category,
                                            create_date_start=create_date_start,
                                            create_date_end=create_date_end, update_date_start=update_date_start,
                                            update_date_end=update_date_end)
        return list(loanproducts)
@route.get('/list', response=List[SectionSchema], auth=JWTAuth())
    @paginate()
    def get_section_list(
            self,
            section_id: str = None,
            section_name: str = None,
            section_minimum_score: int = None,
            create_date_start: str = None,
            create_date_end: str = None,
            update_date_start: str = None,
            update_date_end: str = None
    ):
        query = Q()
        dateformat = '%Y-%m-%d'

        sections = get_filtered_objects(Section, query, dateformat, id=section_id, name=section_name,
                                        minimum_score=section_minimum_score, create_date_start=create_date_start,
                                        create_date_end=create_date_end, update_date_start=update_date_start,
                                        update_date_end=update_date_end)

        sections = sections.annotate(
            question_count=Count('question')
        )

        return list(sections)

Code Coverage

Code coverage menunjukan seberapa banyak bagian code yang ter-cover oleh unit test. Semakin tinggi code coverage, maka semakin banyak behavior program yang di-test. SonarQube juga dapat melakukan analisis code coverage dengan menunjukan persentase code coverage baik untuk program keseluruhan ataupun untuk setiap file.

Persentase code coverage untuk program keseluruhan

Persentase code coverage untuk setiap file

Security Hotspot

Security hotspot menunjukan bagian code yang berpotensi mengandung masalah security. SonarQube dapat menganalisis hal ini dan menunjukan secara detail letak permasalahannya. Sebagai contoh, hasil analisis SonarQube menunjukan terdapat masalah security pada code proyek PPL saya, tepatnya pada bagian CORS allowed origin yang mengizinkan request dari semua sumber (tenang saja, hal tersebut sudah di-fix pada code production :D)

Hasil analisis SonarQube menunjukan security hotspot pada bagian CORS policy

SonarLint

SonarLint merupakan tool lain yang dapat membantu kita menganalisis kualitas program. Tidak seperti SonarQube yang menjalankan analisis pada server, SonarLint berjalan secara lokal dan biasanya terintegrasi dengan IDE seperti Visual Studio Code, PyCharm, Intellij IDEA. SonarLint dapat membantu kita menemukan code smell secara realtime saat sedang men-develop program.

Analisis code smell secara realtime menggunakan SonarLint

Kesimpulan

SonarQube merupakan salah satu tools analisis kualitas code. Dengan menggunakan tools tersebut, beban developer yang sebelumnya harus melakukan analisis secara manual dapat berkurang. SonarQube dapat menganalisis berbagai hal seperti code smell, duplicated code, code coverage, dan security hotspot.

Referensi

SonarQube 10.0