Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nim Support #160

Merged
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