diff --git a/README.md b/README.md index 9f94294..e7cafaf 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,8 @@ Based on this, here are the supported languages: | R | `*.R` extension. Supports single-line `//` comments and multi-line `/* */` comments | | Rust | `*.rs` extension. Supports single-line `//` comments and multi-line `/* */` comments | | Scala | `*.scala`, `*.sc` extensions. Supports single-line `//` comments and multi-line `/* */` comments | -| Swift | `*.swift` extension. Supports single-line `//` comments and multi-line `/* */` comments | +| Swift | `*.swift` extension. Supports single-line `//` comments and multi-line `/* */` comments +| Vue | `*.vue` extension. Supports single-line `//` comments, multi-line `/* */` comments and multi-line `` HTML comments | If you don't see your favorite language in this table, but it does use one of the supported comment formats, submit an issue [here](https://github.com/preslavmihaylov/todocheck/issues/new) diff --git a/matchers/matchers.go b/matchers/matchers.go index d59f153..66c238a 100644 --- a/matchers/matchers.go +++ b/matchers/matchers.go @@ -10,6 +10,7 @@ import ( "github.com/preslavmihaylov/todocheck/matchers/scripts" "github.com/preslavmihaylov/todocheck/matchers/standard" "github.com/preslavmihaylov/todocheck/matchers/state" + "github.com/preslavmihaylov/todocheck/matchers/vue" ) // TodoMatcher for todo comments @@ -124,6 +125,23 @@ var ( return groovy.NewCommentMatcher(callback) }, } + + vueMatcherFactory = &matcherFactory{ + func() func([]string) TodoMatcher { + var once sync.Once + var matcher TodoMatcher + + return func(customTodos []string) TodoMatcher { + once.Do(func() { + matcher = vue.NewTodoMatcher(customTodos) + }) + return matcher + } + }(), + func(callback state.CommentCallback) CommentMatcher { + return vue.NewCommentMatcher(callback) + }, + } ) var supportedMatchers = map[string]*matcherFactory{ @@ -159,6 +177,9 @@ var supportedMatchers = map[string]*matcherFactory{ // file types, supporting python comments ".py": pythonMatcherFactory, + + // file types, supporting js, html and css comments + ".vue": vueMatcherFactory, } // TodoMatcherForFile gets the correct todo matcher for the given filename diff --git a/matchers/vue/comments.go b/matchers/vue/comments.go new file mode 100644 index 0000000..64e18d0 --- /dev/null +++ b/matchers/vue/comments.go @@ -0,0 +1,144 @@ +package vue + +import ( + "github.com/preslavmihaylov/todocheck/matchers/state" +) + +// NewCommentMatcher for vue comments +func NewCommentMatcher(callback state.CommentCallback) *CommentMatcher { + return &CommentMatcher{ + callback: callback, + } +} + +// CommentMatcher for vue comments +type CommentMatcher struct { + callback state.CommentCallback + buffer string + lines []string + linecnt int + stringToken rune + isExitingMultilineComment bool + commentType string + isStartingHTML bool +} + +// NonCommentState for vue comments +func (m *CommentMatcher) NonCommentState( + filename, line string, linecnt int, prevToken, currToken, nextToken rune, +) (state.CommentState, error) { + if prevToken == '/' && currToken == '/' { + m.buffer += string(currToken) + + return state.SingleLineComment, nil + } else if currToken == '"' || currToken == '\'' { + m.stringToken = currToken + + return state.String, nil + } else if prevToken == '/' && currToken == '*' { + m.buffer += "/*" + m.lines = []string{line} + m.linecnt = linecnt + m.commentType = "CSS" + + return state.MultiLineComment, nil + } else if prevToken == '<' && currToken == '!' && nextToken == '-' { + m.isStartingHTML = true + + return state.NonComment, nil + } else if m.isStartingHTML && nextToken == '-' { + m.buffer += "' { + return true + } + } + return false +} diff --git a/matchers/vue/doc.go b/matchers/vue/doc.go new file mode 100644 index 0000000..c0991da --- /dev/null +++ b/matchers/vue/doc.go @@ -0,0 +1,3 @@ +// Package vue contains a todo matcher & comments matcher for vue comments. +// Vue single-line comments use the '//' literal, and the multi-line comments use the CS&JS /**/ or the HTML literals. +package vue diff --git a/matchers/vue/todomatcher.go b/matchers/vue/todomatcher.go new file mode 100644 index 0000000..e25bd63 --- /dev/null +++ b/matchers/vue/todomatcher.go @@ -0,0 +1,62 @@ +package vue + +import ( + "regexp" + + "github.com/preslavmihaylov/todocheck/common" + "github.com/preslavmihaylov/todocheck/matchers/errors" +) + +// NewTodoMatcher for vue comments +func NewTodoMatcher(todos []string) *TodoMatcher { + pattern := common.ArrayAsRegexAnyMatchExpression(todos) + + singleLineTodoPattern := regexp.MustCompile(`^\s*//.*` + pattern) + singleLineValidTodoPattern := regexp.MustCompile(`^\s*// ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`) + + 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 vue comments +type TodoMatcher struct { + singleLineTodoPattern *regexp.Regexp + singleLineValidTodoPattern *regexp.Regexp + multiLineTodoPattern *regexp.Regexp + multiLineValidTodoPattern *regexp.Regexp +} + +// IsMatch checks if the current expression matches a vue 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") +} diff --git a/testing/scenarios/vue/main.vue b/testing/scenarios/vue/main.vue new file mode 100644 index 0000000..93a5b28 --- /dev/null +++ b/testing/scenarios/vue/main.vue @@ -0,0 +1,55 @@ +// oneline comment, malformed TODO +// TODO 1: oneline comment wellformed +// TODO 2: oneline comment with Issue closed + + + + + +/* TODO 1: wellformed CS/JS multiline entry */ +/* TODO 2: wellformed CS/JS multiline entry, BUT issue closed */ +/* +wellformed CS/JS multline entry +*/ + +/* +TODO: malformed CS/JS multline entry, missing number +*/ + + + + +"this is a // TODO 2: valid comment in a string with issue closed" +"this is a /* TODO 2: valid multiline comment in a string with issue closed */" +'this is a ' + + /* TODO 1: valid multiline comment in code */ + + + + + + diff --git a/testing/todocheck_test.go b/testing/todocheck_test.go index 594844f..decc24a 100644 --- a/testing/todocheck_test.go +++ b/testing/todocheck_test.go @@ -742,6 +742,66 @@ func TestPrintingVersionFlagStopsProgram(t *testing.T) { } } +func TestVueTodos(t *testing.T) { + err := scenariobuilder.NewScenario(). + WithBinary("../todocheck"). + WithBasepath("./scenarios/vue"). + WithConfig("./test_configs/no_issue_tracker.yaml"). + WithIssueTracker(issuetracker.Jira). + WithIssue("1", issuetracker.StatusOpen). + WithIssue("2", issuetracker.StatusClosed). + WithIssue("3", issuetracker.StatusOpen). + WithIssue("4", issuetracker.StatusOpen). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeMalformed). + WithLocation("scenarios/vue/main.vue", 1). + ExpectLine("// oneline comment, malformed TODO")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeIssueClosed). + WithLocation("scenarios/vue/main.vue", 3). + ExpectLine("// TODO 2: oneline comment with Issue closed")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeIssueClosed). + WithLocation("scenarios/vue/main.vue", 6). + ExpectLine("")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeMalformed). + WithLocation("scenarios/vue/main.vue", 7). + ExpectLine("")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeIssueClosed). + WithLocation("scenarios/vue/main.vue", 10). + ExpectLine("/* TODO 2: wellformed CS/JS multiline entry, BUT issue closed */")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeMalformed). + WithLocation("scenarios/vue/main.vue", 15). + ExpectLine("/*"). + ExpectLine("TODO: malformed CS/JS multline entry, missing number"). + ExpectLine("*/")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeIssueClosed). + WithLocation("scenarios/vue/main.vue", 49). + ExpectLine(" color: blue; // TODO 2: online comment wellformed in code, BUT issue closed")). + ExpectTodoErr( + scenariobuilder.NewTodoErr(). + WithType(errors.TODOErrTypeMalformed). + WithLocation("scenarios/vue/main.vue", 53). + ExpectLine("")). + Run() + if err != nil { + t.Errorf("%s", err) + } +} + // Testing multiple todo matchers created for different file types func TestMultipleTodoMatchers(t *testing.T) { err := scenariobuilder.NewScenario().