Creating a Syntax Checking Tool for C and C++ using C language

A4s1kk
14 min readAug 9, 2024

--

As developers, ensuring the quality of code is paramount. One of the key aspects of code quality is adhering to proper syntax rules. To assist in this process, we’ve developed a tool designed to analyze and validate the syntax of C and C++ codebases. This tool is capable of detecting common syntax errors, providing detailed reports, and calculating cyclomatic complexity, making it an invaluable resource for C and C++ developers.

Tool Overview

The tool consists of two main components:

  1. C Program (main.c): This program reads and analyzes C/C++ source files, performing various syntax checks and generating a detailed report.
  2. Makefile: A simple script to automate the build process using gcc.

The C Program: main.c

Let’s dive into the key features of the C program:

  • Line-by-Line Analysis: The program reads each line of the input file, storing relevant information like line number, length, and content. This helps in identifying and reporting errors with pinpoint accuracy.
  • Bracket Matching: One of the common issues in C/C++ programming is mismatched brackets. The tool checks for balanced curly braces and reports any discrepancies.
  • Keyword Detection: It scans for important C/C++ keywords like int, float, class, and others, ensuring that these keywords are used appropriately.
  • Function and Prototype Counting: The program identifies functions and prototypes, counting their occurrences to help developers get a quick overview of the code’s structure.
  • Built-in Functions: It checks for the usage of built-in functions like malloc, free, new, and delete, helping to spot potential memory management issues.
  • Print/Scan Functions: The tool identifies the usage of printf, scanf, cout, and cin, ensuring that input/output operations are correctly handled.
  • Cyclomatic Complexity Calculation: Cyclomatic complexity is a metric used to measure the complexity of a program. The tool calculates this value, providing insights into how complex and potentially difficult to maintain the code might be.

The Makefile

To automate the compilation process, we’ve included a Makefile. This script streamlines the process of building the executable by specifying the compiler, flags, sources, and object files.

  • Compiler and Flags: The Makefile uses gcc with several flags, including -Wall and -Wextra for comprehensive warnings, -std=c11 for standard compliance, and -Iheaders to include header files.
  • Sources and Objects: The source files and corresponding object files are defined, allowing the Makefile to track dependencies and ensure everything is compiled correctly.
  • Build Target (all): The all target compiles the entire program, producing an executable named code_analysis_tool.
  • Clean Target: The clean target removes any compiled objects and the executable, making it easy to start fresh.

Let’s walk through each section of the code to understand how the syntax-checking tool is structured and functions.

Part 1: C Program (main.c)

  1. 1: Libraries and Constants
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#define MAX_LINE_LENGTH 1024

Libraries: The program begins by including standard libraries:

  • stdio.h for input/output operations.
  • stdlib.h for memory allocation and process control.
  • string.h for string manipulation functions.
  • stdbool.h for boolean data types (true and false).

Constant: MAX_LINE_LENGTH defines the maximum length for each line of code that the program will process.

1.2: Main Function

int main(int argc, char *argv[]) {
FILE *file, *output;
char line[MAX_LINE_LENGTH];
int line_number = 0;
int open_braces = 0, close_braces = 0;

Parameters:

  • argc is the argument count.
  • argv is an array of arguments passed to the program from the command line.

Variables:

  • file and output are file pointers.
  • line stores each line of code as it’s read from the input file.
  • line_number tracks the current line number.
  • open_braces and close_braces count the number of { and } encountered.

1.3: File Handling

if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file>\n", argv[0]);
return 1;
}

file = fopen(argv[1], "r");
if (!file) {
fprintf(stderr, "Error opening file %s\n", argv[1]);
return 1;
}

output = fopen("output.txt", "w");
if (!output) {
fprintf(stderr, "Error opening output file\n");
fclose(file);
return 1;
}

Argument Check: Ensures that exactly one argument (the source file) is passed.

File Opening:

  • Opens the source file in read mode ("r").
  • If the file can’t be opened, an error message is printed, and the program exits.
  • Opens an output file named output.txt in write mode ("w").

1.4: Reading and Processing Lines

while (fgets(line, MAX_LINE_LENGTH, file)) {
line_number++;
int line_length = strlen(line);
fprintf(output, "Line %d: %s", line_number, line);

// Checking for open and close braces
for (int i = 0; i < line_length; i++) {
if (line[i] == '{') open_braces++;
else if (line[i] == '}') close_braces++;
}

if (open_braces != close_braces) {
fprintf(output, "Mismatched braces detected.\n");
}

// Checking for certain keywords
if (strstr(line, "int") || strstr(line, "float") || strstr(line, "double") ||
strstr(line, "char") || strstr(line, "void") || strstr(line, "class") ||
strstr(line, "struct")) {
fprintf(output, "Declaration found in Line %d\n", line_number);
}

// Checking for function prototypes and definitions
if (strchr(line, '(') && strchr(line, ')') && strchr(line, ';')) {
fprintf(output, "Function prototype found in Line %d\n", line_number);
} else if (strchr(line, '(') && strchr(line, ')') && strchr(line, '{')) {
fprintf(output, "Function definition found in Line %d\n", line_number);
}

// Checking for built-in functions
if (strstr(line, "malloc") || strstr(line, "free") ||
strstr(line, "new") || strstr(line, "delete")) {
fprintf(output, "Memory management function detected in Line %d\n", line_number);
}

// Checking for I/O functions
if (strstr(line, "printf") || strstr(line, "scanf") ||
strstr(line, "cout") || strstr(line, "cin")) {
fprintf(output, "I/O function detected in Line %d\n", line_number);
}
}
  • Reading Lines: The fgets function reads each line from the source file.
  • Line Numbering: The line_number is incremented for each line.

Brace Matching:

  • The program checks for { and } to ensure they are balanced.
  • If mismatched braces are detected, a warning is written to the output file.

Keyword Detection:

  • Looks for common C/C++ keywords (e.g., int, float, class) and identifies declarations.

Function Prototype/Definition Detection:

  • Checks for (, ), and ; to identify function prototypes.
  • Checks for { to identify function definitions.

Built-in Functions: Scans for memory management functions like malloc, free, new, and delete.

I/O Functions: Scans for functions like printf, scanf, cout, and cin.

1.5: Cyclomatic Complexity Calculation

int cyclomatic_complexity = 1;
rewind(file);
while (fgets(line, MAX_LINE_LENGTH, file)) {
if (strstr(line, "if") || strstr(line, "else if") || strstr(line, "while") ||
strstr(line, "for") || strstr(line, "switch") || strstr(line, "case")) {
cyclomatic_complexity++;
}
}
fprintf(output, "Cyclomatic complexity: %d\n", cyclomatic_complexity);
  • Rewind File: rewind resets the file pointer to the beginning of the file.

Complexity Calculation:

  • Checks for control statements (if, while, for, switch) and increments cyclomatic_complexity.
  • Writes the cyclomatic complexity value to the output file.

1.6: Cleanup

fclose(file);
fclose(output);
return 0;
  • Close Files: Closes the input and output files.
  • Exit Program: The program exits with a status of 0 (success).

Complete main.c:

// Author: Aas1kkk
// Date: 2024-07-13
// Description: A tool designed to analyze and validate the syntax of C and C++ codebases. It ensures code quality by detecting common syntax errors and providing detailed reports.
// File version: 1.2
// Last Update: 2024-07-17
// License: GNU License
// Recent changes: Added support for multiple file inputs, C++ specific constructs, improved output format, and added cyclomatic complexity calculation.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

// Structure to store each line of the file along with its line number and length
typedef struct {
int line_number;
int line_length;
char line_text[1024];
} FileLine;

// Function declarations
void print_lines(FileLine lines[], int total_lines, FILE *output_file);
int find_comment_position(char line[], int line_length);
void check_brackets(FileLine lines[], int total_lines, FILE *output_file);
void check_keywords(FileLine lines[], int total_lines, FILE *output_file, int is_cpp);
void count_functions_and_prototypes(FileLine lines[], int total_lines, FILE *output_file);
void check_keyword_usage(FileLine lines[], int total_lines, FILE *output_file, int is_cpp);
void check_builtin_functions(FileLine lines[], int total_lines, FILE *output_file, int is_cpp);
void check_print_scan_functions(FileLine lines[], int total_lines, FILE *output_file);
int is_print_function(char line[], int line_length);
int is_scan_function(char line[], int line_length);
void count_variables(FileLine lines[], int total_lines, FILE *output_file);
void check_file_operations(FileLine lines[], int total_lines, FILE *output_file);
int is_for_loop(char *line, int length);
int is_while_loop(char *line, int length);
int is_valid_function_syntax(char *line);
int is_valid_variable_declaration(char *line);
void check_semicolons(FileLine lines[], int total_lines, FILE *output_file);
void check_cpp_specific_constructs(FileLine lines[], int total_lines, FILE *output_file);
void check_class_usage(FileLine lines[], int total_lines, FILE *output_file);
void check_templates(FileLine lines[], int total_lines, FILE *output_file);
void analyze_file(const char *input_filename, FILE *output_file);
int calculate_cyclomatic_complexity(FileLine lines[], int total_lines);

// Function to process a single file
// Function to process a single file
void analyze_file(const char *input_filename, FILE *output_file) {
FILE *input_file;
FileLine *lines = NULL;
char buffer[1024];
int total_lines = 0, line_length, comment_position;
int is_cpp = 0;
int capacity = 100; // Initial capacity

lines = (FileLine *)malloc(capacity * sizeof(FileLine));
if (lines == NULL) {
fprintf(output_file, "Error: Memory allocation failed.\n");
return;
}

// Determine file type based on extension
if (strstr(input_filename, ".cpp") != NULL) {
is_cpp = 1;
} else if (strstr(input_filename, ".c") == NULL) {
fprintf(output_file, "Error: Unsupported file extension for file %s. Please use .c or .cpp files.\n", input_filename);
free(lines);
return;
}

input_file = fopen(input_filename, "r");
if (input_file == NULL) {
fprintf(output_file, "Error: Could not open input file %s.\n", input_filename);
free(lines);
return;
}

// Read lines from the input file
while (fgets(buffer, sizeof(buffer), input_file) != NULL) {
if (total_lines >= capacity) {
capacity *= 2;
lines = (FileLine *)realloc(lines, capacity * sizeof(FileLine));
if (lines == NULL) {
fprintf(output_file, "Error: Memory reallocation failed.\n");
fclose(input_file);
return;
}
}

line_length = strlen(buffer); // Get the length of the line
comment_position = find_comment_position(buffer, line_length); // Find position of comment if exists

// Process the line based on the presence of comments
if (buffer[0] != '\n' && comment_position == -1) {
lines[total_lines].line_number = total_lines + 1;
lines[total_lines].line_length = line_length;
strcpy(lines[total_lines].line_text, buffer);
total_lines++;
} else if (buffer[0] != '\n' && comment_position != -1) {
lines[total_lines].line_number = total_lines + 1;
strncpy(lines[total_lines].line_text, buffer, comment_position);
lines[total_lines].line_text[comment_position] = '\0';
lines[total_lines].line_length = comment_position;
total_lines++;
}
}

fclose(input_file);

// Perform various checks and write results to the output file
fprintf(output_file, "Analysis for file: %s\n", input_filename);
print_lines(lines, total_lines, output_file);
check_brackets(lines, total_lines, output_file);
check_keywords(lines, total_lines, output_file, is_cpp);
count_functions_and_prototypes(lines, total_lines, output_file);
check_keyword_usage(lines, total_lines, output_file, is_cpp);
check_builtin_functions(lines, total_lines, output_file, is_cpp);
check_print_scan_functions(lines, total_lines, output_file);
count_variables(lines, total_lines, output_file);
check_file_operations(lines, total_lines, output_file);
check_semicolons(lines, total_lines, output_file);

if (is_cpp) {
check_cpp_specific_constructs(lines, total_lines, output_file);
}

int cyclomatic_complexity = calculate_cyclomatic_complexity(lines, total_lines);
fprintf(output_file, "Cyclomatic Complexity: %d\n", cyclomatic_complexity);

fprintf(output_file, "\n");

// Free allocated memory
free(lines);
}

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <source_file1> <source_file2> ... <source_fileN>\n", argv[0]);
return 1;
}

FILE *output_file = fopen("output.txt", "w");
if (output_file == NULL) {
printf("Error: Could not open output file.\n");
return 1;
}

// Process each file passed as an argument
for (int i = 1; i < argc; i++) {
analyze_file(argv[i], output_file);
}

fclose(output_file);

return 0;
}

// Function to print the lines
void print_lines(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
fprintf(output_file, "Line %d: %s", lines[i].line_number, lines[i].line_text);
}
}

// Function to find the position of a comment in a line
int find_comment_position(char line[], int line_length) {
for (int i = 0; i < line_length - 1; i++) {
if (line[i] == '/' && line[i + 1] == '/') {
return i;
}
}
return -1;
}

// Function to check for matching brackets
void check_brackets(FileLine lines[], int total_lines, FILE *output_file) {
int open_brackets = 0, close_brackets = 0;
for (int i = 0; i < total_lines; i++) {
for (int j = 0; j < lines[i].line_length; j++) {
if (lines[i].line_text[j] == '{') open_brackets++;
if (lines[i].line_text[j] == '}') close_brackets++;
}
}
if (open_brackets != close_brackets) {
fprintf(output_file, "Error: Mismatched brackets detected.\n");
} else {
fprintf(output_file, "Brackets are balanced.\n");
}
}

// Function to check for specific keywords in the code
void check_keywords(FileLine lines[], int total_lines, FILE *output_file, int is_cpp) {
const char *keywords_c[] = {"int", "float", "if", "else", "while", "for", "return"};
const char *keywords_cpp[] = {"class", "public", "private", "protected", "new", "delete", "namespace", "template"};
int keyword_count_c = sizeof(keywords_c) / sizeof(keywords_c[0]);
int keyword_count_cpp = sizeof(keywords_cpp) / sizeof(keywords_cpp[0]);

for (int i = 0; i < total_lines; i++) {
for (int j = 0; j < keyword_count_c; j++) {
if (strstr(lines[i].line_text, keywords_c[j])) {
fprintf(output_file, "Line %d: Found keyword '%s'\n", lines[i].line_number, keywords_c[j]);
}
}
if (is_cpp) {
for (int j = 0; j < keyword_count_cpp; j++) {
if (strstr(lines[i].line_text, keywords_cpp[j])) {
fprintf(output_file, "Line %d: Found keyword '%s'\n", lines[i].line_number, keywords_cpp[j]);
}
}
}
}
}

// Function to count functions and prototypes
void count_functions_and_prototypes(FileLine lines[], int total_lines, FILE *output_file) {
int function_count = 0;
int prototype_count = 0;

for (int i = 0; i < total_lines; i++) {
if (is_valid_function_syntax(lines[i].line_text)) {
if (strchr(lines[i].line_text, ';')) {
prototype_count++;
} else {
function_count++;
}
}
}
fprintf(output_file, "Number of functions: %d\n", function_count);
fprintf(output_file, "Number of function prototypes: %d\n", prototype_count);
}

// Function to check keyword usage
void check_keyword_usage(FileLine lines[], int total_lines, FILE *output_file, int is_cpp) {
for (int i = 0; i < total_lines; i++) {
if (is_for_loop(lines[i].line_text, lines[i].line_length)) {
fprintf(output_file, "Line %d: Contains a for loop\n", lines[i].line_number);
}
if (is_while_loop(lines[i].line_text, lines[i].line_length)) {
fprintf(output_file, "Line %d: Contains a while loop\n", lines[i].line_number);
}
}
}

// Function to check for the usage of built-in functions
void check_builtin_functions(FileLine lines[], int total_lines, FILE *output_file, int is_cpp) {
const char *builtin_functions_c[] = {"malloc", "calloc", "free", "exit", "qsort", "bsearch"};
const char *builtin_functions_cpp[] = {"new", "delete"};
int function_count_c = sizeof(builtin_functions_c) / sizeof(builtin_functions_c[0]);
int function_count_cpp = sizeof(builtin_functions_cpp) / sizeof(builtin_functions_cpp[0]);

for (int i = 0; i < total_lines; i++) {
for (int j = 0; j < function_count_c; j++) {
if (strstr(lines[i].line_text, builtin_functions_c[j])) {
fprintf(output_file, "Line %d: Found built-in function usage '%s'\n", lines[i].line_number, builtin_functions_c[j]);
}
}
if (is_cpp) {
for (int j = 0; j < function_count_cpp; j++) {
if (strstr(lines[i].line_text, builtin_functions_cpp[j])) {
fprintf(output_file, "Line %d: Found built-in function usage '%s'\n", lines[i].line_number, builtin_functions_cpp[j]);
}
}
}
}
}


// Function to check the usage of print and scan functions
void check_print_scan_functions(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
if (is_print_function(lines[i].line_text, lines[i].line_length)) {
fprintf(output_file, "Line %d: Contains a print function\n", lines[i].line_number);
}
if (is_scan_function(lines[i].line_text, lines[i].line_length)) {
fprintf(output_file, "Line %d: Contains a scan function\n", lines[i].line_number);
}
}
}

// Function to check if a line contains a print function
int is_print_function(char line[], int line_length) {
if (strstr(line, "printf") || strstr(line, "cout")) {
return 1;
}
return 0;
}

// Function to check if a line contains a scan function
int is_scan_function(char line[], int line_length) {
if (strstr(line, "scanf") || strstr(line, "cin")) {
return 1;
}
return 0;
}

// Function to count the number of variables
void count_variables(FileLine lines[], int total_lines, FILE *output_file) {
const char *data_types[] = {"int", "float", "double", "char"};
int data_type_count = sizeof(data_types) / sizeof(data_types[0]);
int variable_count = 0;

for (int i = 0; i < total_lines; i++) {
for (int j = 0; j < data_type_count; j++) {
if (strstr(lines[i].line_text, data_types[j])) {
variable_count++;
break;
}
}
}
fprintf(output_file, "Number of variables: %d\n", variable_count);
}

// Function to check file operations
void check_file_operations(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
if (strstr(lines[i].line_text, "fopen")) {
fprintf(output_file, "Line %d: Contains a file open operation\n", lines[i].line_number);
}
if (strstr(lines[i].line_text, "fclose")) {
fprintf(output_file, "Line %d: Contains a file close operation\n", lines[i].line_number);
}
}
}

// Function to check if a line contains a for loop
int is_for_loop(char *line, int length) {
if (strstr(line, "for")) {
return 1;
}
return 0;
}

// Function to check if a line contains a while loop
int is_while_loop(char *line, int length) {
if (strstr(line, "while")) {
return 1;
}
return 0;
}

// Function to check if a line has valid function syntax
int is_valid_function_syntax(char *line) {
if (strchr(line, '(') && strchr(line, ')') && strchr(line, '{')) {
return 1;
}
return 0;
}

// Function to check if a line has valid variable declaration
int is_valid_variable_declaration(char *line) {
const char *data_types[] = {"int", "float", "double", "char"};
int data_type_count = sizeof(data_types) / sizeof(data_types[0]);

for (int i = 0; i < data_type_count; i++) {
if (strstr(line, data_types[i])) {
return 1;
}
}
return 0;
}

// Function to check for missing semicolons
void check_semicolons(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
if (!strchr(lines[i].line_text, ';') && !strstr(lines[i].line_text, "for") && !strstr(lines[i].line_text, "while") && !strstr(lines[i].line_text, "{") && !strstr(lines[i].line_text, "}")) {
fprintf(output_file, "Line %d: Missing semicolon\n", lines[i].line_number);
}
}
}

// Function to check for C++ specific constructs
void check_cpp_specific_constructs(FileLine lines[], int total_lines, FILE *output_file) {
check_class_usage(lines, total_lines, output_file);
check_templates(lines, total_lines, output_file);
}

// Function to check for class usage in C++
void check_class_usage(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
if (strstr(lines[i].line_text, "class")) {
fprintf(output_file, "Line %d: Contains class declaration\n", lines[i].line_number);
}
}
}

// Function to check for template usage in C++
void check_templates(FileLine lines[], int total_lines, FILE *output_file) {
for (int i = 0; i < total_lines; i++) {
if (strstr(lines[i].line_text, "template")) {
fprintf(output_file, "Line %d: Contains template usage\n", lines[i].line_number);
}
}
}

// Function to calculate cyclomatic complexity
int calculate_cyclomatic_complexity(FileLine lines[], int total_lines) {
int complexity = 1; // Cyclomatic complexity starts at 1
for (int i = 0; i < total_lines; i++) {
if (strstr(lines[i].line_text, "if") || strstr(lines[i].line_text, "else if") ||
strstr(lines[i].line_text, "for") || strstr(lines[i].line_text, "while") ||
strstr(lines[i].line_text, "case") || strstr(lines[i].line_text, "default") ||
strstr(lines[i].line_text, "&&") || strstr(lines[i].line_text, "||")) {
complexity++;
}
}
return complexity;
}

Part 2: Makefile

The Makefile is used to automate the compilation of the C program.

2.1: Variables

CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -Iheaders
SOURCES = main.c helpers/style_check.c
OBJECTS = $(SOURCES:.c=.o)
EXECUTABLE = code_analysis_tool
  • CC: Defines the compiler to be gcc.

CFLAGS: Compiler flags:

  • -Wall and -Wextra enable warnings.
  • -std=c11 enforces the C11 standard.
  • -Iheaders specifies the directory for header files.

SOURCES: Lists the source files.

OBJECTS: Converts the source files (.c) to object files (.o).

EXECUTABLE: Defines the name of the final executable.

2.2: Build Targets

all: $(EXECUTABLE)
  • all: The default target, builds the executable.

2.3: Building the Executable

$(EXECUTABLE): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^

Executable Target: Links the object files to create the executable.

  • $(CC) invokes the compiler.
  • $(CFLAGS) applies the compiler flags.
  • -o $@ specifies the output file (the executable).
  • $^ includes all object files.

2.4: Clean Target

clean:
rm -f $(OBJECTS) $(EXECUTABLE)
  • clean: Deletes the object files and the executable, allowing for a fresh build.

Complete Makefile:

CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -Iheaders
SOURCES = main.c helpers/style_check.c
OBJECTS = $(SOURCES:.c=.o)
EXECUTABLE = code_analysis_tool

all: $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^

clean:
rm -f $(OBJECTS) $(EXECUTABLE)

Check the full repo from here: https://github.com/aa-sikkkk/C-syntaxChecker

How to Use the Tool

  1. Compiling the Tool:
  • Run make in your terminal. This command compiles the source files and generates the code_analysis_tool executable.

2. Running the Tool:

  • Execute the tool by passing the source files you want to analyze as arguments. For example:
./code_analysis_tool file1.c file2.cpp
  • The tool will analyze the files and generate a report in output.txt.

3. Cleaning Up

  • To clean the project directory, run:
make clean

Conclusion

The syntax-checking tool you’ve built is a robust solution for analyzing C/C++ code. The main.c program performs detailed syntax analysis, and the Makefile automates the compilation process. This tool can help developers maintain high-quality code by detecting common syntax errors, misused keywords, and calculating cyclomatic complexity.

Ready to Level Up Your Python Skills?

EscapeMantra: The Ultimate Python Ebook” is here to guide you through every step of mastering Python. Whether you’re new to coding or looking to sharpen your skills, this ebook is packed with practical examples, hands-on exercises, and real-world projects to make learning both effective and enjoyable.

Here’s what you’ll get:

  • Clear Explanations: Understand Python concepts easily with straightforward guidance.
  • Engaging Projects: Work on fun projects like a Snake game and an AI Chatbot to apply what you’ve learned.
  • Hands-On Practice: Build your skills with exercises designed to boost your confidence.

👉 Grab your copy. Dive in today and start mastering Python at your own pace. Don’t wait — your programming journey starts now!

🚀 Support My Work and Get More Exclusive Content! 🚀

If you found article helpful and want to see more in-depth content, tools, and exclusive resources, consider supporting me on Patreon. Your support helps me create and share valuable content, improve projects, and build a community of passionate developers.

👉 Become a Patron Today! Join here to access exclusive source codes, early project releases, and more!

Thank you for your support and for being part of this journey!

--

--