Skip to content

Commit

Permalink
feat: support include and exclude keywords options
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Oct 23, 2024
1 parent 9bb0ddf commit 68027cb
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 20 deletions.
2 changes: 1 addition & 1 deletion preview/blocks/info.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
style={`--color: var(--color-additional-${colors[index]});`}
class="legend-color"
></div>
<Typography size="s" tag="span">{kind}</Typography>
<Typography size="s" tag="span">{kind.toUpperCase()}</Typography>
</div>
{/each}
</div>
Expand Down
51 changes: 51 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ npx todoctor

The program will automatically collect data and display the history of `TODO` / `FIXME` comments across commits.

## Report

### Todos Graph

After running the tool, it generates a detailed graph showing the evolution of TODO comments over time. The graph visualizes how many todo comments were added, resolved, or modified across the project's history.

This helps you track the technical debt and maintenance progress at a glance.
Expand All @@ -67,6 +71,8 @@ This helps you track the technical debt and maintenance progress at a glance.
/>
</picture>

### Additional Information

In addition to the graph, the tool provides insightful statistics, such as:

- The total number of todo comments.
Expand All @@ -92,6 +98,8 @@ These insights help you better understand the state of your codebase and priorit
/>
</picture>

### List of Todos

Finally, the tool generates a detailed list of all todo comments in your project in a tabular format.

The list includes the comment text, the file path, and additional metadata, such as line numbers and authorship information. This list helps you identify, review, and manage unresolved tasks more effectively.
Expand Down Expand Up @@ -136,6 +144,49 @@ Example:
todoctor --ignore src/deprecated/ --ignore tests/legacy.test.js
```

### --include-keywords

Allows you to specify additional keywords in comments that will be treated as technical debt. This option can be used multiple times.

Example:

```sh
todoctor --include-keywords eslint-disable-next-line
```

### --exclude-keywords

Allows you to exclude keywords from the report. By default, the following keywords are used to define the technical debt comment:

- `TODO`
- `FIXME`
- `XXX`
- `HACK`
- `BUG`
- `OPTIMIZE`
- `REFACTOR`
- `TEMP`
- `CHANGED`
- `IDEA`
- `NOTE`
- `REVIEW`
- `NB`
- `QUESTION`
- `DEBUG`
- `KLUDGE`
- `COMPAT`
- `WARNING`
- `DANGER`
- `INFO`
- `DEPRECATED`
- `COMBAK`

Example:

```sh
todoctor --exclude-keywords WARNING --exclude-keywords DEPRECATED
```

### --help

Displays this help message with available options.
Expand Down
41 changes: 40 additions & 1 deletion src/identify_todo_comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,57 @@ lazy_static! {
Regex::new(r"(?i)^[^\w]*(\w+)([\s\p{P}]*)(:|[\p{P}\s]|$)").unwrap();
}

pub fn identify_todo_comment(comment_text: &str) -> Option<String> {
pub fn identify_todo_comment(
comment_text: &str,
include_keywords: Option<&[&str]>,
exclude_keywords: Option<&[&str]>,
) -> Option<String> {
let trimmed_text = comment_text.trim();

if let Some(included) = include_keywords {
for keyword in included {
let re = Regex::new(&format!(r"(?i){}", regex::escape(keyword)))
.unwrap();
if re.is_match(trimmed_text) {
return Some(keyword.to_string());
}
}
}

for (i, re) in PRIMARY_KEYWORD_REGEXES.iter().enumerate() {
if re.is_match(trimmed_text) {
if let Some(excluded) = exclude_keywords {
if excluded.iter().any(|&keyword| {
PRIMARY_TODO_KEYWORDS[i].eq_ignore_ascii_case(keyword)
}) {
return None;
}
}
return Some(PRIMARY_TODO_KEYWORDS[i].to_string());
}
}

if let Some(captures) = SECONDARY_KEYWORD_REGEX.captures(trimmed_text) {
let first_word = captures.get(1).unwrap().as_str();

if let Some(excluded) = exclude_keywords {
if excluded
.iter()
.any(|&keyword| first_word.eq_ignore_ascii_case(keyword))
{
return None;
}
}

if let Some(excluded) = exclude_keywords {
if excluded
.iter()
.any(|&keyword| first_word.eq_ignore_ascii_case(keyword))
{
return None;
}
}

for keyword in SECONDARY_TODO_KEYWORDS.iter() {
if first_word.eq_ignore_ascii_case(keyword) {
return Some(keyword.to_string());
Expand Down
67 changes: 64 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ const TODOCTOR_DIR: &str = "todoctor";
const HISTORY_TEMP_FILE: &str = "todo_history_temp.json";

#[derive(Parser, Debug)]
#[command(name = "todoctor", about = "Tracks TODO comments in code")]
#[command(
name = "todoctor",
about = "
Todoctor is a powerful tool for analyzing, tracking, and visualizing technical
debt in your codebase using Git. It collects and monitors TODO/FIXME comments
in your code, allowing you to observe changes over time."
)]
struct Cli {
/// Number of months to process
#[arg(short, long, default_value_t = 3, value_parser = clap::value_parser!(u32).range(1..))]
Expand All @@ -43,6 +49,14 @@ struct Cli {
/// Paths to ignore (can be used multiple times)
#[arg(short, long, action = ArgAction::Append)]
ignore: Vec<String>,

/// Keywords to track for TODO comments (can be used multiple times)
#[arg(short = 'I', long, action = ArgAction::Append)]
include_keywords: Vec<String>,

/// Keywords to exclude from tracking (can be used multiple times)
#[arg(short = 'E', long, action = ArgAction::Append)]
exclude_keywords: Vec<String>,
}

#[tokio::main]
Expand All @@ -63,6 +77,16 @@ async fn main() {
.map(|values| values.map(String::from).collect())
.unwrap_or_else(Vec::new);

let include_keywords: Vec<String> = args
.get_many::<String>("include_keywords")
.map(|values| values.map(String::from).collect())
.unwrap_or_else(Vec::new);

let exclude_keywords: Vec<String> = args
.get_many::<String>("exclude_keywords")
.map(|values| values.map(String::from).collect())
.unwrap_or_else(Vec::new);

if !check_git_repository(".").await {
eprintln!("Error: This is not a Git repository.");
process::exit(1);
Expand All @@ -81,15 +105,33 @@ async fn main() {
let todo_data_tasks: Vec<_> = files
.into_iter()
.map(|source_file_name: String| {
let include_keywords = include_keywords.clone();
let exclude_keywords = exclude_keywords.clone();

tokio::spawn(async move {
match fs::read_to_string(&source_file_name).await {
Ok(source) => {
let comments = get_comments(&source, &source_file_name);
comments
.into_iter()
.filter_map(|comment| {
let include_keywords_refs: Vec<&str> =
include_keywords
.iter()
.map(|s| s.as_str())
.collect();
let exclude_keywords_refs: Vec<&str> =
exclude_keywords
.iter()
.map(|s| s.as_str())
.collect();

if let Some(comment_kind) =
identify_todo_comment(&comment.text)
identify_todo_comment(
&comment.text,
Some(&include_keywords_refs),
Some(&exclude_keywords_refs),
)
{
Some(TodoData {
path: source_file_name.clone(),
Expand Down Expand Up @@ -202,6 +244,10 @@ async fn main() {
.map(|file_path| {
let semaphore = semaphore.clone();
let commit_hash = commit_hash.clone();

let include_keywords = include_keywords.clone();
let exclude_keywords = exclude_keywords.clone();

tokio::spawn(async move {
let permit = semaphore.acquire_owned().await.unwrap();

Expand All @@ -213,10 +259,25 @@ async fn main() {
.await
{
let comments = get_comments(&file_content, &file_path);

let include_keywords_refs: Vec<&str> = include_keywords
.iter()
.map(|s| s.as_str())
.collect();
let exclude_keywords_refs: Vec<&str> = exclude_keywords
.iter()
.map(|s| s.as_str())
.collect();

let todos: Vec<_> = comments
.into_iter()
.filter(|comment| {
identify_todo_comment(&comment.text).is_some()
identify_todo_comment(
&comment.text,
Some(&include_keywords_refs),
Some(&exclude_keywords_refs),
)
.is_some()
})
.collect();

Expand Down
Loading

0 comments on commit 68027cb

Please sign in to comment.