Skip to content

Commit

Permalink
Nim Support (#160)
Browse files Browse the repository at this point in the history
* Register matcher for Nim style comments (#157)

* Implement initial support for finding single line and multiline comments (#157)

* Add files for testing (#157)

* Rework depth calculation (#157)

In particular, trigger end of multiline whenever m.depth == 0

* Remove extra test case (#157)

* Corrected single line matching pattern (#157)

* Tests now pass (#157)

* Register all file extensions (#157)

* Add Nim to README (#157)

* Finish adding support for Nim comments (#157)
  • Loading branch information
bengsparks authored Aug 15, 2021
1 parent 818469f commit d24d39d
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ Based on this, here are the supported languages:
| Java | `*.java` extension. Supports single-line `//` comments and multi-line `/* */` comments |
| JavaScript/Typescript | `*.js/*.ts` extension. Supports single-line `//` comments and multi-line `/* */` comments |
| Kotlin | `*.kt/*.kts/*.ktm` extension. Supports single-line `//` comments and multi-line `/* */` comments |
| Nim | `*.{nim, nims, nimble}` extension. Supports single-line `#` comments and multi-line `#[ ]#` comments |
| PHP | `*.php` extension. Supports single-line `#` and `//` comments and multi-line `/* */` comments |
| Python | `*.py` extension. Supports single-line `#` comments and multi-line `"""` comments |
| R | `*.R` extension. Supports single-line `//` comments and multi-line `/* */` comments |
Expand Down
23 changes: 22 additions & 1 deletion matchers/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"sync"

"github.com/preslavmihaylov/todocheck/matchers/groovy"
"github.com/preslavmihaylov/todocheck/matchers/nim"
"github.com/preslavmihaylov/todocheck/matchers/php"
"github.com/preslavmihaylov/todocheck/matchers/python"
"github.com/preslavmihaylov/todocheck/matchers/scripts"
Expand Down Expand Up @@ -125,7 +126,6 @@ var (
return groovy.NewCommentMatcher(callback)
},
}

vueMatcherFactory = &matcherFactory{
func() func([]string) TodoMatcher {
var once sync.Once
Expand All @@ -142,6 +142,22 @@ var (
return vue.NewCommentMatcher(callback)
},
}
nimMatcherFactory = &matcherFactory{
func() func([]string) TodoMatcher {
var once sync.Once
var matcher TodoMatcher

return func(customTodos []string) TodoMatcher {
once.Do(func() {
matcher = nim.NewTodoMatcher(customTodos)
})
return matcher
}
}(),
func(callback state.CommentCallback) CommentMatcher {
return nim.NewCommentMatcher(callback)
},
}
)

var supportedMatchers = map[string]*matcherFactory{
Expand Down Expand Up @@ -183,6 +199,11 @@ var supportedMatchers = map[string]*matcherFactory{

// file types, supporting js, html and css comments
".vue": vueMatcherFactory,

// file types, supporting nim comments
".nim": nimMatcherFactory,
".nims": nimMatcherFactory,
".nimble": nimMatcherFactory,
}

// TodoMatcherForFile gets the correct todo matcher for the given filename
Expand Down
121 changes: 121 additions & 0 deletions matchers/nim/comments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package nim

import (
"github.com/preslavmihaylov/todocheck/matchers/state"
)

func NewCommentMatcher(callback state.CommentCallback) *CommentMatcher {
return &CommentMatcher{
callback: callback,
depth: 0,
}
}

type CommentMatcher struct {
callback state.CommentCallback
buffer string
lines []string
lineCount int
stringToken rune
depth int
}

func (m *CommentMatcher) NonCommentState(
filename,
line string,
lineCount int,
prevToken, currToken, nextToken rune,
) (state.CommentState, error) {
if isSingleLineOpener(currToken, nextToken) {
m.buffer += string(currToken)
return state.SingleLineComment, nil
} else if isMultiLineOpener(currToken, nextToken) {
m.buffer += string(currToken)
m.lines = []string{line}
m.lineCount = lineCount
m.depth += 1
return state.MultiLineComment, nil
} else if currToken == '"' || currToken == '\'' || currToken == '`' {
m.stringToken = currToken
return state.String, nil
} else {
return state.NonComment, nil
}
}

func (m *CommentMatcher) MultiLineCommentState(
filename, line string, linecnt int, prevToken, currToken, nextToken rune,
) (state.CommentState, error) {
m.buffer += string(currToken)
if isMultiLineOpener(currToken, nextToken) {
// Capture nested comments
m.depth += 1
} else if isMultiLineCloser(currToken, nextToken) {
m.depth -= 1
}

if m.depth == 0 {
err := m.callback(m.buffer, filename, m.lines, m.lineCount)
if err != nil {
return state.NonComment, err
}

m.reset()
return state.NonComment, nil
}

if prevToken == '\n' {
m.lines = append(m.lines, line)
}

return state.MultiLineComment, nil
}

func (m *CommentMatcher) SingleLineCommentState(
filename, line string, linecnt int, prevToken, currToken, nextToken rune,
) (state.CommentState, error) {
if currToken == '\n' {
// Reach end of line i.e. end of comment
err := m.callback(m.buffer, filename, []string{line}, linecnt)
if err != nil {
return state.NonComment, err
}

m.reset()
return state.NonComment, nil
}

m.buffer += string(currToken)
return state.SingleLineComment, nil
}

// StringState for standard comments
func (m *CommentMatcher) StringState(
filename, line string, linecnt int, prevToken, currToken, nextToken rune,
) (state.CommentState, error) {
if prevToken != '\\' && currToken == m.stringToken {
return state.NonComment, nil
}

return state.String, nil
}

func isSingleLineOpener(currToken, nextToken rune) bool {
return currToken == '#' && nextToken != '['
}

func isMultiLineOpener(currToken, nextToken rune) bool {
return currToken == '#' && nextToken == '['
}

func isMultiLineCloser(currToken, nextToken rune) bool {
return currToken == ']' && nextToken == '#'
}

func (m *CommentMatcher) reset() {
m.buffer = ""
m.lines = nil
m.lineCount = 0
m.stringToken = 0
m.depth = 0
}
3 changes: 3 additions & 0 deletions matchers/nim/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package scripts contains a todo matcher & comments matcher for Nim.
// Source files contain # for single-line comments, and nestable #[ ]# multiline comments
package nim
64 changes: 64 additions & 0 deletions matchers/nim/todomatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package nim

import (
"regexp"

"github.com/preslavmihaylov/todocheck/common"
"github.com/preslavmihaylov/todocheck/matchers/errors"
)

// NewTodoMatcher for Nim comments
func NewTodoMatcher(todos []string) *TodoMatcher {
pattern := common.ArrayAsRegexAnyMatchExpression(todos)

// Single line
singleLineTodoPattern := regexp.MustCompile(`^\s*#[^\[].*` + pattern)
singleLineValidTodoPattern := regexp.MustCompile(`^\s*#[^\[]` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)

// Multiline line
multiLineTodoPattern := regexp.MustCompile(`(?s)^\s*(#\[).*` + pattern)
multiLineValidTodoPattern := regexp.MustCompile(`(?s)^\s*(#\[).*` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)

return &TodoMatcher{
singleLineTodoPattern: singleLineTodoPattern,
singleLineValidTodoPattern: singleLineValidTodoPattern,
multiLineTodoPattern: multiLineTodoPattern,
multiLineValidTodoPattern: multiLineValidTodoPattern,
}
}

// TodoMatcher for Nim comments
type TodoMatcher struct {
singleLineTodoPattern *regexp.Regexp
singleLineValidTodoPattern *regexp.Regexp
multiLineTodoPattern *regexp.Regexp
multiLineValidTodoPattern *regexp.Regexp
}

// IsMatch checks if the current expression matches a Nim comment
func (m *TodoMatcher) IsMatch(expr string) bool {
return m.singleLineTodoPattern.Match([]byte(expr)) || m.multiLineTodoPattern.Match([]byte(expr))
}

// IsValid checks if the expression is a valid todo comment
func (m *TodoMatcher) IsValid(expr string) bool {
return m.singleLineValidTodoPattern.Match([]byte(expr)) || m.multiLineValidTodoPattern.Match([]byte(expr))
}

// ExtractIssueRef from the given expression.
// If the expression is invalid, an ErrInvalidTODO is returned
func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
if !m.IsValid(expr) {
return "", errors.ErrInvalidTODO
}

singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
multiLineRes := m.multiLineValidTodoPattern.FindStringSubmatch(expr)
if len(singleLineRes) >= 2 {
return singleLineRes[1], nil
} else if len(multiLineRes) >= 3 {
return multiLineRes[2], nil
}

panic("Invariant violated. No issue reference found in valid TODO")
}
14 changes: 14 additions & 0 deletions testing/scenarios/nim/main.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This is a single-line malformed TODO

# TODO 1: This is a valid todo comment

when isMainModule:
# TODO 234: Invalid todo, with a closed issue
discard "Hello World"

#[ TODO 2: Another valid todo ]#

discard "Magic here, magic there!"
#[
#[ TODO 3: There is also a valid nested todo here! ]#
]#
38 changes: 38 additions & 0 deletions testing/todocheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,44 @@ func TestPrintingVersionFlagStopsProgram(t *testing.T) {
}
}

func TestNimTodos(t *testing.T) {
err := scenariobuilder.NewScenario().
WithBinary("../todocheck").
WithBasepath("./scenarios/nim").
WithConfig("./test_configs/no_issue_tracker.yaml").
WithIssueTracker(issuetracker.Jira).
WithIssue("1", issuetracker.StatusOpen).
WithIssue("2", issuetracker.StatusClosed).
WithIssue("3", issuetracker.StatusClosed).
WithIssue("234", issuetracker.StatusClosed).
ExpectTodoErr(
scenariobuilder.NewTodoErr().
WithType(errors.TODOErrTypeMalformed).
WithLocation("scenarios/nim/main.nim", 1).
ExpectLine("# This is a single-line malformed TODO")).
ExpectTodoErr(
scenariobuilder.NewTodoErr().
WithType(errors.TODOErrTypeIssueClosed).
WithLocation("scenarios/nim/main.nim", 6).
ExpectLine("# TODO 234: Invalid todo, with a closed issue")).
ExpectTodoErr(
scenariobuilder.NewTodoErr().
WithType(errors.TODOErrTypeIssueClosed).
WithLocation("scenarios/nim/main.nim", 9).
ExpectLine("#[ TODO 2: Another valid todo ]#")).
ExpectTodoErr(
scenariobuilder.NewTodoErr().
WithType(errors.TODOErrTypeIssueClosed).
WithLocation("scenarios/nim/main.nim", 12).
ExpectLine("#[").
ExpectLine("#[ TODO 3: There is also a valid nested todo here! ]#").
ExpectLine("]#")).
Run()
if err != nil {
t.Errorf("%s", err)
}
}

func TestVueTodos(t *testing.T) {
err := scenariobuilder.NewScenario().
WithBinary("../todocheck").
Expand Down

0 comments on commit d24d39d

Please sign in to comment.