Repository: preslavmihaylov/todocheck
Pull request #122 (2020-11-30 17:53:11)

add pivotaltracker CI tests
Similarly to #119 and #99 I'm creating CI tests for pivotaltracker.

For this purpose, I've created an account on their site with read-only access to a pivotaltracker project with a set of hardcoded issues with all various statuses.

For each of them, I've created a separate todo line in the tests in this PR to verify we can correctly unmarshal the response received from their API.

I also changed my mind about removing the verbosity of tests as making them non-verbose turned out to make it quite hard to see where the issue is due to mised standard output from tests and the testing output

The `pull_request_target` tests are failing as they are using the workflow from the master branch, which doesn't include the new secret. This is expected.


Patch:
diff --git a/.github/workflows/master.yaml b/.github/workflows/master.yaml
index 2547eb3..c94c788 100644
--- a/.github/workflows/master.yaml
+++ b/.github/workflows/master.yaml
@@ -29,4 +29,5 @@ jobs:
           TODOCHECK_ENV: "ci"
           TESTS_GITHUB_APITOKEN: ${{ secrets.TESTS_GITHUB_APITOKEN }}
           TESTS_GITLAB_APITOKEN: ${{ secrets.TESTS_GITLAB_APITOKEN }}
+          TESTS_PIVOTALTRACKER_APITOKEN: ${{ secrets.TESTS_PIVOTALTRACKER_APITOKEN }}
         run: make test
diff --git a/Makefile b/Makefile
index 0e6aa32..9a23e8c 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ build:
 	go build -ldflags "-X main.version=$(version)"
 
 test: build
-	go test -count=1 ./testing
+	go test -v -count=1 ./testing
 
 release:
 	@echo "Generating binaries for version $(version)..."
diff --git a/README.md b/README.md
index 62160f8..7d389da 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ See [How it works](#how-it-works) for more info.
   * [Redmine](#redmine)
 - [Supported Programming Languages](#supported-programming-languages)
 - [Ignored Files & Directories](#ignored-files--directories)
+- [Custom todos](#custom-todos)
 - [Supported Output Formats](#supported-output-formats)
 - [Authentication](#authentication)
   * [None](#none)
@@ -242,6 +243,27 @@ ignored:
 Ignored files/folders can be specified via standard pattern-matching.
 Hidden files (dotfiles, i.e. `.git`, `.gitignore`, etc) are ignored by default.
 
+# Custom Todos
+By default, `todocheck` looks for todos in the format `// TODO #231: ...` Most projects stick to this format.
+
+However, if you have some special todo format, which is different from the default one, you can specify a list of custom todo formats in your `.todocheck.yaml`  
+```
+origin: some.origin.com
+issue_tracker: JIRA
+custom_todos:
+  - "@fix"
+  - ToDo
+```
+
+Note that this list adds additional formats the tool should match, but doesn't substitute the default one - `TODO`.
+
+With the above configuration, `todocheck` will start matching todo comments in the following formats:
+```
+// @fix #234: a leftover task...
+// ToDo #15: A funky-looking todo comment
+```
+
+
 # Supported Output Formats
 Currently, todocheck supports two kinds of output - standard & json.  
 
@@ -360,6 +382,7 @@ In your `.todocheck.yaml` configuration, you have the following configuration op
  * origin - the origin of your remote repository/issue tracker (example: `github.com/golang/go`)
  * issue_tracker - the issue tracker type you're using. Possible options - `GITHUB`, `GITLAB`, `JIRA`
  * ignored - a list of directories/files todocheck should ignore. Supports pattern-macthing, e.g. `*.sh`.
+ * custom_todos - a list of custom todos variables. `TODO` will always be added. (example: `"@fix"`)
  * auth - the authentication configuration for your issue tracker. If not present, it defaults to auth `type: none`
    * type - the type of authentication. Possible options - `none` (default), `offline`, `apitoken`A
    * offline_url - the url for fetching offline tokens. Only used when type is `offline`
diff --git a/common/common.go b/common/common.go
index 34391f0..7de5352 100644
--- a/common/common.go
+++ b/common/common.go
@@ -1,6 +1,11 @@
 // Package common contains a bunch of small utility functions used throughout different independent other packages
 package common
 
+import (
+	"fmt"
+	"strings"
+)
+
 // RemoveEmptyTokens of a splitted string. Tokens such as "" are removed.
 func RemoveEmptyTokens(ss []string) []string {
 	res := []string{}
@@ -12,3 +17,11 @@ func RemoveEmptyTokens(ss []string) []string {
 
 	return res
 }
+
+// ArrayAsRegexAnyMatchExpression converting array to regexp string for matching any of elements
+func ArrayAsRegexAnyMatchExpression(todos []string) string {
+	if len(todos) == 0 {
+		panic("Empty list of todo strings")
+	}
+	return fmt.Sprintf("(?:%s)", strings.Join(todos, "|"))
+}
diff --git a/config/config.go b/config/config.go
index 849a5c0..3029884 100644
--- a/config/config.go
+++ b/config/config.go
@@ -23,6 +23,7 @@ type Local struct {
 	Origin       string       `yaml:"origin"`
 	IssueTracker IssueTracker `yaml:"issue_tracker"`
 	IgnoredPaths []string     `yaml:"ignored"`
+	CustomTodos  []string     `yaml:"custom_todos"`
 	Auth         *Auth        `yaml:"auth"`
 }
 
@@ -55,6 +56,9 @@ func NewLocal(cfgPath, basepath string) (*Local, error) {
 	trimTrailingSlashesFromDirs(cfg.IgnoredPaths)
 	removeCurrentDirReference(cfg.IgnoredPaths)
 
+	cfg.CustomTodos = decodeEscapedReservedCharacters(cfg.CustomTodos)
+	cfg.CustomTodos = addDefaultFormatIfMissing(cfg.CustomTodos)
+
 	return cfg, nil
 }
 
@@ -154,3 +158,32 @@ func removeCurrentDirReference(dirs []string) {
 func isRelativePath(path string) bool {
 	return path[0] != '/' && path[0] != '~' && !windowsAbsolutePathPattern.MatchString(path)
 }
+
+// addDefaultFormatIfMissing trying find default TODO string and adding it if not exists
+func addDefaultFormatIfMissing(todos []string) []string {
+	var isExists bool
+	for _, v := range todos {
+		if v == "TODO" {
+			isExists = true
+			continue
+		}
+	}
+
+	if !isExists {
+		todos = append(todos, "TODO")
+	}
+
+	return todos
+}
+
+func decodeEscapedReservedCharacters(slice []string) []string {
+	// Remove leading escaping "\" for reserved strings.
+	// "@", "`" in YAML is reserved indicators
+	//  https://yaml.org/spec/1.2/spec.html#id2772075
+	for i, v := range slice {
+		if strings.HasPrefix(v, "\\@") || strings.HasPrefix(v, "\\`") {
+			slice[i] = v[1:]
+		}
+	}
+	return slice
+}
diff --git a/main.go b/main.go
index bcda630..792d58c 100644
--- a/main.go
+++ b/main.go
@@ -67,7 +67,7 @@ func main() {
 	f := fetcher.NewFetcher(tracker, authmiddleware.For(localCfg))
 
 	todoErrs := []*todocheckerrors.TODO{}
-	traverser := todoerrs.NewTraverser(f, localCfg.IgnoredPaths, func(todoErr *todocheckerrors.TODO) error {
+	traverser := todoerrs.NewTraverser(f, localCfg.IgnoredPaths, localCfg.CustomTodos, func(todoErr *todocheckerrors.TODO) error {
 		todoErrs = append(todoErrs, todoErr)
 		return nil
 	})
diff --git a/matchers/groovy/todomatcher.go b/matchers/groovy/todomatcher.go
index 056bcde..22460ad 100644
--- a/matchers/groovy/todomatcher.go
+++ b/matchers/groovy/todomatcher.go
@@ -3,29 +3,46 @@ package groovy
 import (
 	"regexp"
 
+	"github.com/preslavmihaylov/todocheck/common"
 	"github.com/preslavmihaylov/todocheck/matchers/errors"
 )
 
-var singleLineTodoPattern = regexp.MustCompile("^\\s*//.*TODO")
-var singleLineValidTodoPattern = regexp.MustCompile("^\\s*// TODO (#?[a-zA-Z0-9\\-]+):.*")
+// NewTodoMatcher for groovy comments
+func NewTodoMatcher(todos []string) *TodoMatcher {
+	pattern := common.ArrayAsRegexAnyMatchExpression(todos)
 
-var multiLineTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO")
-var multiLineValidTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO (#?[a-zA-Z0-9\\-]+):.*")
+	// Single line
+	singleLineTodoPattern := regexp.MustCompile(`^\s*//.*` + pattern)
+	singleLineValidTodoPattern := regexp.MustCompile(`^\s*// ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
 
-// NewTodoMatcher for groovy comments
-func NewTodoMatcher() *TodoMatcher { return &TodoMatcher{} }
+	// 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 groovy comments
-type TodoMatcher struct{}
+type TodoMatcher struct {
+	singleLineTodoPattern      *regexp.Regexp
+	singleLineValidTodoPattern *regexp.Regexp
+	multiLineTodoPattern       *regexp.Regexp
+	multiLineValidTodoPattern  *regexp.Regexp
+}
 
 // IsMatch checks if the current expression matches a groovy comment
 func (m *TodoMatcher) IsMatch(expr string) bool {
-	return singleLineTodoPattern.Match([]byte(expr)) || multiLineTodoPattern.Match([]byte(expr))
+	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 singleLineValidTodoPattern.Match([]byte(expr)) || multiLineValidTodoPattern.Match([]byte(expr))
+	return m.singleLineValidTodoPattern.Match([]byte(expr)) || m.multiLineValidTodoPattern.Match([]byte(expr))
 }
 
 // ExtractIssueRef from the given expression.
@@ -35,8 +52,8 @@ func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
 		return "", errors.ErrInvalidTODO
 	}
 
-	singleLineRes := singleLineValidTodoPattern.FindStringSubmatch(expr)
-	multiLineRes := multiLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
+	multiLineRes := m.multiLineValidTodoPattern.FindStringSubmatch(expr)
 	if len(singleLineRes) >= 2 {
 		return singleLineRes[1], nil
 	} else if len(multiLineRes) >= 2 {
diff --git a/matchers/matchers.go b/matchers/matchers.go
index e5be081..d59f153 100644
--- a/matchers/matchers.go
+++ b/matchers/matchers.go
@@ -2,6 +2,7 @@ package matchers
 
 import (
 	"path/filepath"
+	"sync"
 
 	"github.com/preslavmihaylov/todocheck/matchers/groovy"
 	"github.com/preslavmihaylov/todocheck/matchers/php"
@@ -28,55 +29,97 @@ type CommentMatcher interface {
 }
 
 type matcherFactory struct {
-	newTodoMatcher     func() TodoMatcher
+	newTodoMatcher     func([]string) TodoMatcher
 	newCommentsMatcher func(callback state.CommentCallback) CommentMatcher
 }
 
 var (
 	standardMatcherFactory = &matcherFactory{
-		func() TodoMatcher {
-			return standard.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = standard.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return standard.NewCommentMatcher(callback, false)
 		},
 	}
 	standardMatcherWithNestedMultilineCommentsFactory = &matcherFactory{
-		func() TodoMatcher {
-			return standard.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = standard.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return standard.NewCommentMatcher(callback, true)
 		},
 	}
 	scriptsMatcherFactory = &matcherFactory{
-		func() TodoMatcher {
-			return scripts.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = scripts.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return scripts.NewCommentMatcher(callback)
 		},
 	}
 	phpMatcherFactory = &matcherFactory{
-		func() TodoMatcher {
-			return php.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = php.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return php.NewCommentMatcher(callback)
 		},
 	}
 	pythonMatcherFactory = &matcherFactory{
-		func() TodoMatcher {
-			return python.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = python.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return python.NewCommentMatcher(callback)
 		},
 	}
 	groovyMatcherFactory = &matcherFactory{
-		func() TodoMatcher {
-			return groovy.NewTodoMatcher()
-		},
+		func() func([]string) TodoMatcher {
+			var once sync.Once
+			var matcher TodoMatcher
+			return func(customTodos []string) TodoMatcher {
+				once.Do(func() {
+					matcher = groovy.NewTodoMatcher(customTodos)
+				})
+				return matcher
+			}
+		}(),
 		func(callback state.CommentCallback) CommentMatcher {
 			return groovy.NewCommentMatcher(callback)
 		},
@@ -119,10 +162,10 @@ var supportedMatchers = map[string]*matcherFactory{
 }
 
 // TodoMatcherForFile gets the correct todo matcher for the given filename
-func TodoMatcherForFile(filename string) TodoMatcher {
+func TodoMatcherForFile(filename string, todos []string) TodoMatcher {
 	extension := filepath.Ext(filename)
 	if matcherFactory, ok := supportedMatchers[extension]; ok {
-		return matcherFactory.newTodoMatcher()
+		return matcherFactory.newTodoMatcher(todos)
 	}
 
 	return nil
diff --git a/matchers/php/todomatcher.go b/matchers/php/todomatcher.go
index 5485083..4089969 100644
--- a/matchers/php/todomatcher.go
+++ b/matchers/php/todomatcher.go
@@ -3,36 +3,58 @@ package php
 import (
 	"regexp"
 
+	"github.com/preslavmihaylov/todocheck/common"
 	"github.com/preslavmihaylov/todocheck/matchers/errors"
 )
 
-var singleLineTodoPattern = regexp.MustCompile("^\\s*//.*TODO")
-var singleLineValidTodoPattern = regexp.MustCompile("^\\s*// TODO (#?[a-zA-Z0-9\\-]+):.*")
+// NewTodoMatcher for php comments
+func NewTodoMatcher(todos []string) *TodoMatcher {
+	pattern := common.ArrayAsRegexAnyMatchExpression(todos)
 
-var singleLineScriptTodoPattern = regexp.MustCompile("^\\s*#.*TODO")
-var singleLineScriptValidTodoPattern = regexp.MustCompile("^\\s*# TODO (#?[a-zA-Z0-9\\-]+):.*")
+	// Single line
+	singleLineTodoPattern := regexp.MustCompile(`^\s*//.*` + pattern)
+	singleLineValidTodoPattern := regexp.MustCompile(`^\s*// ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
 
-var multiLineTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO")
-var multiLineValidTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO (#?[a-zA-Z0-9\\-]+):.*")
+	// Script line
+	singleLineScriptTodoPattern := regexp.MustCompile(`^\s*#.*` + pattern)
+	singleLineScriptValidTodoPattern := regexp.MustCompile(`^\s*# ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
 
-// NewTodoMatcher for php comments
-func NewTodoMatcher() *TodoMatcher { return &TodoMatcher{} }
+	// Multiline line
+	multiLineTodoPattern := regexp.MustCompile(`(?s)^\s*/\*.*` + pattern)
+	multiLineValidTodoPattern := regexp.MustCompile(`(?s)^\s*/\*.*` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
+
+	return &TodoMatcher{
+		singleLineTodoPattern:            singleLineTodoPattern,
+		singleLineValidTodoPattern:       singleLineValidTodoPattern,
+		singleLineScriptTodoPattern:      singleLineScriptTodoPattern,
+		singleLineScriptValidTodoPattern: singleLineScriptValidTodoPattern,
+		multiLineTodoPattern:             multiLineTodoPattern,
+		multiLineValidTodoPattern:        multiLineValidTodoPattern,
+	}
+}
 
 // TodoMatcher for php comments
-type TodoMatcher struct{}
+type TodoMatcher struct {
+	singleLineTodoPattern            *regexp.Regexp
+	singleLineValidTodoPattern       *regexp.Regexp
+	singleLineScriptTodoPattern      *regexp.Regexp
+	singleLineScriptValidTodoPattern *regexp.Regexp
+	multiLineTodoPattern             *regexp.Regexp
+	multiLineValidTodoPattern        *regexp.Regexp
+}
 
 // IsMatch checks if the current expression matches a standard comment
 func (m *TodoMatcher) IsMatch(expr string) bool {
-	return singleLineTodoPattern.Match([]byte(expr)) ||
-		singleLineScriptTodoPattern.Match([]byte(expr)) ||
-		multiLineTodoPattern.Match([]byte(expr))
+	return m.singleLineTodoPattern.Match([]byte(expr)) ||
+		m.singleLineScriptTodoPattern.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 singleLineValidTodoPattern.Match([]byte(expr)) ||
-		singleLineScriptValidTodoPattern.Match([]byte(expr)) ||
-		multiLineValidTodoPattern.Match([]byte(expr))
+	return m.singleLineValidTodoPattern.Match([]byte(expr)) ||
+		m.singleLineScriptValidTodoPattern.Match([]byte(expr)) ||
+		m.multiLineValidTodoPattern.Match([]byte(expr))
 }
 
 // ExtractIssueRef from the given expression.
@@ -42,9 +64,9 @@ func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
 		return "", errors.ErrInvalidTODO
 	}
 
-	singleLineRes := singleLineValidTodoPattern.FindStringSubmatch(expr)
-	singleLineScriptRes := singleLineScriptValidTodoPattern.FindStringSubmatch(expr)
-	multiLineRes := multiLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineScriptRes := m.singleLineScriptValidTodoPattern.FindStringSubmatch(expr)
+	multiLineRes := m.multiLineValidTodoPattern.FindStringSubmatch(expr)
 	if len(singleLineRes) >= 2 {
 		return singleLineRes[1], nil
 	} else if len(singleLineScriptRes) >= 2 {
diff --git a/matchers/python/todomatcher.go b/matchers/python/todomatcher.go
index 0f467eb..384f812 100644
--- a/matchers/python/todomatcher.go
+++ b/matchers/python/todomatcher.go
@@ -3,29 +3,46 @@ package python
 import (
 	"regexp"
 
+	"github.com/preslavmihaylov/todocheck/common"
 	"github.com/preslavmihaylov/todocheck/matchers/errors"
 )
 
-var singleLineTodoPattern = regexp.MustCompile("^\\s*#.*TODO")
-var singleLineValidTodoPattern = regexp.MustCompile("^\\s*# TODO (#?[a-zA-Z0-9\\-]+):.*")
+// NewTodoMatcher for python comments
+func NewTodoMatcher(todos []string) *TodoMatcher {
+	pattern := common.ArrayAsRegexAnyMatchExpression(todos)
 
-var multiLineTodoPattern = regexp.MustCompile("(?s)^\\s*(\"\"\"|''').*TODO")
-var multiLineValidTodoPattern = regexp.MustCompile("(?s)^\\s*(\"\"\"|''').*TODO (#?[a-zA-Z0-9\\-]+):.*")
+	// Single line
+	singleLineTodoPattern := regexp.MustCompile(`^\s*#.*` + pattern)
+	singleLineValidTodoPattern := regexp.MustCompile(`^\s*# ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
 
-// NewTodoMatcher for python comments
-func NewTodoMatcher() *TodoMatcher { return &TodoMatcher{} }
+	// 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 python comments
-type TodoMatcher struct{}
+type TodoMatcher struct {
+	singleLineTodoPattern      *regexp.Regexp
+	singleLineValidTodoPattern *regexp.Regexp
+	multiLineTodoPattern       *regexp.Regexp
+	multiLineValidTodoPattern  *regexp.Regexp
+}
 
 // IsMatch checks if the current expression matches a python comment
 func (m *TodoMatcher) IsMatch(expr string) bool {
-	return singleLineTodoPattern.Match([]byte(expr)) || multiLineTodoPattern.Match([]byte(expr))
+	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 singleLineValidTodoPattern.Match([]byte(expr)) || multiLineValidTodoPattern.Match([]byte(expr))
+	return m.singleLineValidTodoPattern.Match([]byte(expr)) || m.multiLineValidTodoPattern.Match([]byte(expr))
 }
 
 // ExtractIssueRef from the given expression.
@@ -35,8 +52,8 @@ func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
 		return "", errors.ErrInvalidTODO
 	}
 
-	singleLineRes := singleLineValidTodoPattern.FindStringSubmatch(expr)
-	multiLineRes := multiLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
+	multiLineRes := m.multiLineValidTodoPattern.FindStringSubmatch(expr)
 	if len(singleLineRes) >= 2 {
 		return singleLineRes[1], nil
 	} else if len(multiLineRes) >= 3 {
diff --git a/matchers/scripts/todomatcher.go b/matchers/scripts/todomatcher.go
index 7a5f3ac..e3ff4c8 100644
--- a/matchers/scripts/todomatcher.go
+++ b/matchers/scripts/todomatcher.go
@@ -3,26 +3,38 @@ package scripts
 import (
 	"regexp"
 
+	"github.com/preslavmihaylov/todocheck/common"
 	"github.com/preslavmihaylov/todocheck/matchers/errors"
 )
 
-var singleLineTodoPattern = regexp.MustCompile("^\\s*#.*TODO")
-var singleLineValidTodoPattern = regexp.MustCompile("^\\s*# TODO (#?[a-zA-Z0-9\\-]+):.*")
-
 // NewTodoMatcher for scripts comments
-func NewTodoMatcher() *TodoMatcher { return &TodoMatcher{} }
+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\-]+):.*`)
+
+	return &TodoMatcher{
+		singleLineTodoPattern:      singleLineTodoPattern,
+		singleLineValidTodoPattern: singleLineValidTodoPattern,
+	}
+}
 
 // TodoMatcher for scripts comments
-type TodoMatcher struct{}
+type TodoMatcher struct {
+	singleLineTodoPattern      *regexp.Regexp
+	singleLineValidTodoPattern *regexp.Regexp
+}
 
 // IsMatch checks if the current expression matches a standard comment
 func (m *TodoMatcher) IsMatch(expr string) bool {
-	return singleLineTodoPattern.Match([]byte(expr))
+	return m.singleLineTodoPattern.Match([]byte(expr))
 }
 
 // IsValid checks if the expression is a valid todo comment
 func (m *TodoMatcher) IsValid(expr string) bool {
-	return singleLineValidTodoPattern.Match([]byte(expr))
+	return m.singleLineValidTodoPattern.Match([]byte(expr))
 }
 
 // ExtractIssueRef from the given expression.
@@ -32,7 +44,7 @@ func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
 		return "", errors.ErrInvalidTODO
 	}
 
-	singleLineRes := singleLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
 	if len(singleLineRes) >= 2 {
 		return singleLineRes[1], nil
 	}
diff --git a/matchers/standard/todomatcher.go b/matchers/standard/todomatcher.go
index f244cfe..984053a 100644
--- a/matchers/standard/todomatcher.go
+++ b/matchers/standard/todomatcher.go
@@ -3,29 +3,46 @@ package standard
 import (
 	"regexp"
 
+	"github.com/preslavmihaylov/todocheck/common"
 	"github.com/preslavmihaylov/todocheck/matchers/errors"
 )
 
-var singleLineTodoPattern = regexp.MustCompile("^\\s*//.*TODO")
-var singleLineValidTodoPattern = regexp.MustCompile("^\\s*// TODO (#?[a-zA-Z0-9\\-]+):.*")
+// NewTodoMatcher for standard comments
+func NewTodoMatcher(todos []string) *TodoMatcher {
+	pattern := common.ArrayAsRegexAnyMatchExpression(todos)
 
-var multiLineTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO")
-var multiLineValidTodoPattern = regexp.MustCompile("(?s)^\\s*/\\*.*TODO (#?[a-zA-Z0-9\\-]+):.*")
+	// Single line
+	singleLineTodoPattern := regexp.MustCompile(`^\s*//.*` + pattern)
+	singleLineValidTodoPattern := regexp.MustCompile(`^\s*// ` + pattern + ` (#?[a-zA-Z0-9\-]+):.*`)
 
-// NewTodoMatcher for standard comments
-func NewTodoMatcher() *TodoMatcher { return &TodoMatcher{} }
+	// 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 standard comments
-type TodoMatcher struct{}
+type TodoMatcher struct {
+	singleLineTodoPattern      *regexp.Regexp
+	singleLineValidTodoPattern *regexp.Regexp
+	multiLineTodoPattern       *regexp.Regexp
+	multiLineValidTodoPattern  *regexp.Regexp
+}
 
 // IsMatch checks if the current expression matches a standard comment
 func (m *TodoMatcher) IsMatch(expr string) bool {
-	return singleLineTodoPattern.Match([]byte(expr)) || multiLineTodoPattern.Match([]byte(expr))
+	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 singleLineValidTodoPattern.Match([]byte(expr)) || multiLineValidTodoPattern.Match([]byte(expr))
+	return m.singleLineValidTodoPattern.Match([]byte(expr)) || m.multiLineValidTodoPattern.Match([]byte(expr))
 }
 
 // ExtractIssueRef from the given expression.
@@ -35,8 +52,8 @@ func (m *TodoMatcher) ExtractIssueRef(expr string) (string, error) {
 		return "", errors.ErrInvalidTODO
 	}
 
-	singleLineRes := singleLineValidTodoPattern.FindStringSubmatch(expr)
-	multiLineRes := multiLineValidTodoPattern.FindStringSubmatch(expr)
+	singleLineRes := m.singleLineValidTodoPattern.FindStringSubmatch(expr)
+	multiLineRes := m.multiLineValidTodoPattern.FindStringSubmatch(expr)
 	if len(singleLineRes) >= 2 {
 		return singleLineRes[1], nil
 	} else if len(multiLineRes) >= 2 {
diff --git a/traverser/todoerrs/todoerrs.go b/traverser/todoerrs/todoerrs.go
index 87a8f68..02bc22a 100644
--- a/traverser/todoerrs/todoerrs.go
+++ b/traverser/todoerrs/todoerrs.go
@@ -15,9 +15,9 @@ import (
 type TodoErrCallback func(todoerr *errors.TODO) error
 
 // NewTraverser for todo errors
-func NewTraverser(f *fetcher.Fetcher, ignoredPaths []string, callback TodoErrCallback) *Traverser {
+func NewTraverser(f *fetcher.Fetcher, ignoredPaths, customTodos []string, callback TodoErrCallback) *Traverser {
 	return &Traverser{
-		comments.NewTraverser(ignoredPaths, commentsCallback(checker.New(f), callback)),
+		comments.NewTraverser(ignoredPaths, commentsCallback(checker.New(f), customTodos, callback)),
 	}
 }
 
@@ -26,9 +26,9 @@ type Traverser struct {
 	commentsTraverser *comments.Traverser
 }
 
-func commentsCallback(chk *checker.Checker, todoErrCallback TodoErrCallback) state.CommentCallback {
+func commentsCallback(chk *checker.Checker, customTodos []string, todoErrCallback TodoErrCallback) state.CommentCallback {
 	return func(comment, filepath string, lines []string, linecnt int) error {
-		todoErr, err := chk.Check(matchers.TodoMatcherForFile(filepath), comment, filepath, lines, linecnt)
+		todoErr, err := chk.Check(matchers.TodoMatcherForFile(filepath, customTodos), comment, filepath, lines, linecnt)
 		if err != nil {
 			return fmt.Errorf("couldn't check todo line: %w", err)
 		} else if todoErr != nil {


Test patch:
diff --git a/testing/customtodos_test.go b/testing/customtodos_test.go
new file mode 100644
index 0000000..a4f6223
--- /dev/null
+++ b/testing/customtodos_test.go
@@ -0,0 +1,650 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/preslavmihaylov/todocheck/checker/errors"
+	"github.com/preslavmihaylov/todocheck/testing/scenariobuilder"
+	"github.com/preslavmihaylov/todocheck/testing/scenariobuilder/issuetracker"
+)
+
+func TestScriptsCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/scripts/").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("123", issuetracker.StatusOpen).
+		WithIssue("321", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 1).
+				ExpectLine("# This is a malformed TODO")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 3).
+				ExpectLine("curl \"localhost:8080\" # This is a TODO comment at the end of the line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 5).
+				ExpectLine("# This is a malformed ToDo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 7).
+				ExpectLine("curl \"localhost:8080\" # This is a ToDo comment at the end of the line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 9).
+				ExpectLine("# This is a malformed @fixme")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 11).
+				ExpectLine("curl \"localhost:8080\" # This is a @fixme comment at the end of the line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 13).
+				ExpectLine("# This is a malformed @fix")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.sh", 15).
+				ExpectLine("curl \"localhost:8080\" # This is a @fix comment at the end of the line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 3).
+				ExpectLine("# A malformed TODO comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 5).
+				ExpectLine("# TODO 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 6).
+				ExpectLine("curl \"localhost:8080\" # TODO 567: This is an invalid todo, marked against a non-existent issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 8).
+				ExpectLine("# A malformed ToDo comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 10).
+				ExpectLine("# ToDo 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 11).
+				ExpectLine("curl \"localhost:8080\" # ToDo 567: This is an invalid todo, marked against a non-existent issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 13).
+				ExpectLine("# A malformed @fixme comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 15).
+				ExpectLine("# @fixme 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 16).
+				ExpectLine("curl \"localhost:8080\" # @fixme 567: This is an invalid todo, marked against a non-existent issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 18).
+				ExpectLine("# A malformed @fix comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 20).
+				ExpectLine("# @fix 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/scripts/script.bash", 21).
+				ExpectLine("curl \"localhost:8080\" # @fix 567: This is an invalid todo, marked against a non-existent issue")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
+func TestStandardCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/standard/").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("123", issuetracker.StatusOpen).
+		WithIssue("321", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 3).
+				ExpectLine("// A malformed TODO comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 5).
+				ExpectLine("// TODO 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/standard/main.go", 6).
+				ExpectLine("/*").
+				ExpectLine(" * TODO 567: This is an invalid multiline todo, marked against a non-existent issue").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 10).
+				ExpectLine("// A malformed ToDo comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 12).
+				ExpectLine("// ToDo 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/standard/main.go", 13).
+				ExpectLine("/*").
+				ExpectLine(" * ToDo 567: This is an invalid multiline todo, marked against a non-existent issue").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 17).
+				ExpectLine("// A malformed @fixme comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 19).
+				ExpectLine("// @fixme 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/standard/main.go", 20).
+				ExpectLine("/*").
+				ExpectLine(" * @fixme 567: This is an invalid multiline todo, marked against a non-existent issue").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 24).
+				ExpectLine("// A malformed @fix comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/standard/main.go", 26).
+				ExpectLine("// @fix 321: This is an invalid todo, marked against a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/standard/main.go", 27).
+				ExpectLine("/*").
+				ExpectLine(" * @fix 567: This is an invalid multiline todo, marked against a non-existent issue").
+				ExpectLine(" */")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
+func TestPythonCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/python").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("1", issuetracker.StatusOpen).
+		WithIssue("234", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 3).
+				ExpectLine("# This is a single-line malformed TODO")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 5).
+				ExpectLine("\"\"\"").
+				ExpectLine("And this is a multiline malformed TODO").
+				ExpectLine("It should be parsed properly").
+				ExpectLine("\"\"\"")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 10).
+				ExpectLine("'''").
+				ExpectLine("This is the same multiline malformed TODO").
+				ExpectLine("but with single-quotes").
+				ExpectLine("'''")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 15).
+				ExpectLine("myvar = 5 # This is a malformed TODO at the end of a line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 19).
+				ExpectLine("hello = \"hello\" # TODO 234: This is an invalid todo, with a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 21).
+				ExpectLine("\"\"\"").
+				ExpectLine("TODO 234: This is an invalid todo, marked against a closed issue").
+				ExpectLine("\"\"\"")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 25).
+				ExpectLine("'''").
+				ExpectLine("TODO 234: This is an invalid todo,").
+				ExpectLine("marked against a closed issue with single quotes").
+				ExpectLine("'''")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 31).
+				ExpectLine("# This is a single-line malformed @fix")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 33).
+				ExpectLine("\"\"\"").
+				ExpectLine("And this is a multiline malformed @fix").
+				ExpectLine("It should be parsed properly").
+				ExpectLine("\"\"\"")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 38).
+				ExpectLine("'''").
+				ExpectLine("This is the same multiline malformed @fix").
+				ExpectLine("but with single-quotes").
+				ExpectLine("'''")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/python/main.py", 43).
+				ExpectLine("myvar = 5 # This is a malformed @fix at the end of a line")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 47).
+				ExpectLine("hello = \"hello\" # @fix 234: This is an invalid todo, with a closed issue")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 49).
+				ExpectLine("\"\"\"").
+				ExpectLine("@fix 234: This is an invalid todo, marked against a closed issue").
+				ExpectLine("\"\"\"")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/python/main.py", 53).
+				ExpectLine("'''").
+				ExpectLine("@fix 234: This is an invalid todo,").
+				ExpectLine("marked against a closed issue with single quotes").
+				ExpectLine("'''")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
+func TestGroovyCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/groovy").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("1", issuetracker.StatusOpen).
+		WithIssue("2", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 1).
+				ExpectLine("//TODO: regular inline comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 11).
+				ExpectLine("/*").
+				ExpectLine("* TODO: Multi-line invalid todo").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 15).
+				ExpectLine("/**").
+				ExpectLine("* TODO: groovydoc invalid todo").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 19).
+				ExpectLine("// TODO 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 21).
+				ExpectLine("// TODO 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 52).
+				ExpectLine("/*").
+				ExpectLine("* TODO 2: Invalid todo as issue is closed").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 56).
+				ExpectLine("/**").
+				ExpectLine("* TODO 2: Invalid todo as issue is closed").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 61).
+				ExpectLine("//@fix: regular inline comment")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 71).
+				ExpectLine("/*").
+				ExpectLine("* @fix: Multi-line invalid todo").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 75).
+				ExpectLine("/**").
+				ExpectLine("* @fix: groovydoc invalid todo").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 79).
+				ExpectLine("// @fix 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 81).
+				ExpectLine("// @fix 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 112).
+				ExpectLine("/*").
+				ExpectLine("* @fix 2: Invalid todo as issue is closed").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/groovy/main.groovy", 116).
+				ExpectLine("/**").
+				ExpectLine("* @fix 2: Invalid todo as issue is closed").
+				ExpectLine("*/")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
+// Also tests Rust TODOs as rust uses the same comment syntax
+func TestSwiftCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/swift").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("1", issuetracker.StatusOpen).
+		WithIssue("2", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 4).
+				ExpectLine("print(\"Hello, World!\") // TODO: An invalid todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 6).
+				ExpectLine("/*").
+				ExpectLine(" * TODO: An invalid multi-line todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 10).
+				ExpectLine("/// TODO: An invalid docstring todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 12).
+				ExpectLine("/*").
+				ExpectLine(" /* TODO: An invalid nested todo */").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 22).
+				ExpectLine("/*").
+				ExpectLine(" /*").
+				ExpectLine("    /* TODO 2: invalid todo as issue is closed */").
+				ExpectLine(" */").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 29).
+				ExpectLine("print(\"Hello, World!\") // @fix: An invalid todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 31).
+				ExpectLine("/*").
+				ExpectLine(" * @fix: An invalid multi-line todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 35).
+				ExpectLine("/// @fix: An invalid docstring todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 37).
+				ExpectLine("/*").
+				ExpectLine(" /* @fix: An invalid nested todo */").
+				ExpectLine("*/")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/swift/main.swift", 47).
+				ExpectLine("/*").
+				ExpectLine(" /*").
+				ExpectLine("    /* @fix 2: invalid todo as issue is closed */").
+				ExpectLine(" */").
+				ExpectLine("*/")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
+func TestPHPCustomTodos(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/custom_todos/php").
+		WithConfig("./test_configs/no_issue_tracker_and_custom_todos.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		WithIssue("1", issuetracker.StatusOpen).
+		WithIssue("2", issuetracker.StatusClosed).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 2).
+				ExpectLine("// TODO: malformed todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 4).
+				ExpectLine("// TODO 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 5).
+				ExpectLine("// TODO 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 7).
+				ExpectLine("# TODO: malformed todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 9).
+				ExpectLine("# TODO 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 10).
+				ExpectLine("# TODO 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 19).
+				ExpectLine("/*").
+				ExpectLine(" * TODO: Multi-line invalid todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 27).
+				ExpectLine("/*").
+				ExpectLine(" * TODO 2: issue is closed").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 31).
+				ExpectLine("/*").
+				ExpectLine(" * TODO 3: issue is non-existent").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 35).
+				ExpectLine("/**").
+				ExpectLine(" * TODO: docstring invalid todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 43).
+				ExpectLine("/**").
+				ExpectLine(" * TODO 2: issue is closed").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 47).
+				ExpectLine("/**").
+				ExpectLine(" * TODO 3: issue is non-existent").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 52).
+				ExpectLine("// @fix: malformed todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 54).
+				ExpectLine("// @fix 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 55).
+				ExpectLine("// @fix 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 57).
+				ExpectLine("# @fix: malformed todo")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 59).
+				ExpectLine("# @fix 2: The issue is closed")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 60).
+				ExpectLine("# @fix 3: The issue is non-existent")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 69).
+				ExpectLine("/*").
+				ExpectLine(" * @fix: Multi-line invalid todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 77).
+				ExpectLine("/*").
+				ExpectLine(" * @fix 2: issue is closed").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 81).
+				ExpectLine("/*").
+				ExpectLine(" * @fix 3: issue is non-existent").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/custom_todos/php/main.php", 85).
+				ExpectLine("/**").
+				ExpectLine(" * @fix: docstring invalid todo").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/custom_todos/php/main.php", 93).
+				ExpectLine("/**").
+				ExpectLine(" * @fix 2: issue is closed").
+				ExpectLine(" */")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/custom_todos/php/main.php", 97).
+				ExpectLine("/**").
+				ExpectLine(" * @fix 3: issue is non-existent").
+				ExpectLine(" */")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
diff --git a/testing/integrations_test.go b/testing/integrations_test.go
index acd05fe..9d21356 100644
--- a/testing/integrations_test.go
+++ b/testing/integrations_test.go
@@ -51,6 +51,44 @@ func TestPrivateGitlabIntegration(t *testing.T) {
 	}
 }
 
+func TestPivotalTrackerIntegration(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		OnlyRunOnCI().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/integrations/pivotaltracker").
+		WithAuthTokenFromEnv("TESTS_PIVOTALTRACKER_APITOKEN").
+		WithConfig("./test_configs/integrations/pivotaltracker.yaml").
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/integrations/pivotaltracker/main.go", 5).
+				ExpectLine("// TODO #175938853: A finished todo (closed)")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/integrations/pivotaltracker/main.go", 7).
+				ExpectLine("// TODO #175938860: A delivered todo (closed)")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/integrations/pivotaltracker/main.go", 11).
+				ExpectLine("// TODO #175938899: A rejected todo (closed)")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeIssueClosed).
+				WithLocation("scenarios/integrations/pivotaltracker/main.go", 13).
+				ExpectLine("// TODO #175938883: An accepted todo (closed)")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeNonExistentIssue).
+				WithLocation("scenarios/integrations/pivotaltracker/main.go", 15).
+				ExpectLine("// TODO #199938883: A non-existent issue")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}
+
 func baseGithubScenario() *scenariobuilder.TodocheckScenario {
 	return scenariobuilder.NewScenario().
 		WithBinary("../todocheck").
diff --git a/testing/scenarios/custom_todos/groovy/main.groovy b/testing/scenarios/custom_todos/groovy/main.groovy
new file mode 100644
index 0000000..fffb25b
--- /dev/null
+++ b/testing/scenarios/custom_todos/groovy/main.groovy
@@ -0,0 +1,118 @@
+//TODO: regular inline comment
+
+/*
+* block comment
+*/
+
+/**
+* groovydoc comment
+*/
+
+/*
+* TODO: Multi-line invalid todo
+*/
+
+/**
+* TODO: groovydoc invalid todo
+*/
+
+// TODO 2: The issue is closed
+
+// TODO 3: The issue is non-existent
+
+def trippleDoubleQuotedString = """string start
+    //TODO: inline comment inside a string block. Shouldn't be captured.
+
+    /* block comment inside a string block */
+    /** groovydoc comment inside a string block */
+string end"""
+
+def trippleSingleQuotedString = '''string2 start
+    //TODO: inline comment inside a string2 block. Shouldn't be captured
+
+    /* block comment inside a string2 block */
+    /** groovydoc comment inside a string2 block */
+string2 end'''
+
+def singleQuotedString = 'string2 start // TODO: inline string shouldn\'t be captured'
+def doubleQuotedString = "string2 start // TODO: inline string \"shouldn't\" be captured"
+
+println trippleDoubleQuotedString
+println ''
+println trippleSingleQuotedString
+
+/**
+* TODO 1: groovydoc valid todo
+*/
+
+/*
+* TODO 1: multi-line valid todo
+*/
+
+/*
+* TODO 2: Invalid todo as issue is closed
+*/
+
+/**
+* TODO 2: Invalid todo as issue is closed
+*/
+
+
+//@fix: regular inline comment
+
+/*
+* block comment
+*/
+
+/**
+* groovydoc comment
+*/
+
+/*
+* @fix: Multi-line invalid todo
+*/
+
+/**
+* @fix: groovydoc invalid todo
+*/
+
+// @fix 2: The issue is closed
+
+// @fix 3: The issue is non-existent
+
+def trippleDoubleQuotedString = """string start
+    //@fix: inline comment inside a string block. Shouldn't be captured.
+
+    /* block comment inside a string block */
+    /** groovydoc comment inside a string block */
+string end"""
+
+def trippleSingleQuotedString = '''string2 start
+    //@fix: inline comment inside a string2 block. Shouldn't be captured
+
+    /* block comment inside a string2 block */
+    /** groovydoc comment inside a string2 block */
+string2 end'''
+
+def singleQuotedString = 'string2 start // @fix: inline string shouldn\'t be captured'
+def doubleQuotedString = "string2 start // @fix: inline string \"shouldn't\" be captured"
+
+println trippleDoubleQuotedString
+println ''
+println trippleSingleQuotedString
+
+/**
+* @fix 1: groovydoc valid todo
+*/
+
+/*
+* @fix 1: multi-line valid todo
+*/
+
+/*
+* @fix 2: Invalid todo as issue is closed
+*/
+
+/**
+* @fix 2: Invalid todo as issue is closed
+*/
diff --git a/testing/scenarios/custom_todos/php/main.php b/testing/scenarios/custom_todos/php/main.php
new file mode 100644
index 0000000..13f5240
--- /dev/null
+++ b/testing/scenarios/custom_todos/php/main.php
@@ -0,0 +1,99 @@
+
+// TODO: malformed todo
+// TODO 1: valid todo
+// TODO 2: The issue is closed
+// TODO 3: The issue is non-existent
+
+# TODO: malformed todo
+# TODO 1: valid todo
+# TODO 2: The issue is closed
+# TODO 3: The issue is non-existent
+
+
+echo 'this is a simple string with a // TODO: comment in it';
+
+echo 'You can also have embedded newlines in
+strings this way as it is
+okay to do. But you shouldn\'t match inline // TODO: comments';
+
+/*
+ * TODO: Multi-line invalid todo
+ */
+
+/*
+ * TODO 1: Multi-line valid todo
+ */
+
+/*
+ * TODO 2: issue is closed
+ */
+
+/*
+ * TODO 3: issue is non-existent
+ */
+
+/**
+ * TODO: docstring invalid todo
+ */
+
+/**
+ * TODO 1: todo is a valid one
+ */
+
+/**
+ * TODO 2: issue is closed
+ */
+
+/**
+ * TODO 3: issue is non-existent
+ */
+
+
+// @fix: malformed todo
+// @fix 1: valid todo
+// @fix 2: The issue is closed
+// @fix 3: The issue is non-existent
+
+# @fix: malformed todo
+# @fix 1: valid todo
+# @fix 2: The issue is closed
+# @fix 3: The issue is non-existent
+
+
+echo 'this is a simple string with a // @fix: comment in it';
+
+echo 'You can also have embedded newlines in
+strings this way as it is
+okay to do. But you shouldn\'t match inline // @fix: comments';
+
+/*
+ * @fix: Multi-line invalid todo
+ */
+
+/*
+ * @fix 1: Multi-line valid todo
+ */
+
+/*
+ * @fix 2: issue is closed
+ */
+
+/*
+ * @fix 3: issue is non-existent
+ */
+
+/**
+ * @fix: docstring invalid todo
+ */
+
+/**
+ * @fix 1: todo is a valid one
+ */
+
+/**
+ * @fix 2: issue is closed
+ */
+
+/**
+ * @fix 3: issue is non-existent
+ */
diff --git a/testing/scenarios/custom_todos/python/main.py b/testing/scenarios/custom_todos/python/main.py
new file mode 100644
index 0000000..3afd741
--- /dev/null
+++ b/testing/scenarios/custom_todos/python/main.py
@@ -0,0 +1,56 @@
+
+
+# This is a single-line malformed TODO
+
+"""
+And this is a multiline malformed TODO
+It should be parsed properly
+"""
+
+'''
+This is the same multiline malformed TODO
+but with single-quotes
+'''
+
+myvar = 5 # This is a malformed TODO at the end of a line
+
+# TODO 1: This is a valid todo comment
+
+hello = "hello" # TODO 234: This is an invalid todo, with a closed issue
+
+"""
+TODO 234: This is an invalid todo, marked against a closed issue
+"""
+
+'''
+TODO 234: This is an invalid todo,
+marked against a closed issue with single quotes
+'''
+
+
+# This is a single-line malformed @fix
+
+"""
+And this is a multiline malformed @fix
+It should be parsed properly
+"""
+
+'''
+This is the same multiline malformed @fix
+but with single-quotes
+'''
+
+myvar = 5 # This is a malformed @fix at the end of a line
+
+# @fix 1: This is a valid todo comment
+
+hello = "hello" # @fix 234: This is an invalid todo, with a closed issue
+
+"""
+@fix 234: This is an invalid todo, marked against a closed issue
+"""
+
+'''
+@fix 234: This is an invalid todo,
+marked against a closed issue with single quotes
+'''
diff --git a/testing/scenarios/custom_todos/scripts/script.bash b/testing/scenarios/custom_todos/scripts/script.bash
new file mode 100644
index 0000000..16a9525
--- /dev/null
+++ b/testing/scenarios/custom_todos/scripts/script.bash
@@ -0,0 +1,21 @@
+# This is in a bash script
+
+# A malformed TODO comment
+# TODO 123: This is a valid todo comment
+# TODO 321: This is an invalid todo, marked against a closed issue
+curl "localhost:8080" # TODO 567: This is an invalid todo, marked against a non-existent issue
+
+# A malformed ToDo comment
+# ToDo 123: This is a valid todo comment
+# ToDo 321: This is an invalid todo, marked against a closed issue
+curl "localhost:8080" # ToDo 567: This is an invalid todo, marked against a non-existent issue
+
+# A malformed @fixme comment
+# @fixme 123: This is a valid todo comment
+# @fixme 321: This is an invalid todo, marked against a closed issue
+curl "localhost:8080" # @fixme 567: This is an invalid todo, marked against a non-existent issue
+
+# A malformed @fix comment
+# @fix 123: This is a valid todo comment
+# @fix 321: This is an invalid todo, marked against a closed issue
+curl "localhost:8080" # @fix 567: This is an invalid todo, marked against a non-existent issue
diff --git a/testing/scenarios/custom_todos/scripts/script.sh b/testing/scenarios/custom_todos/scripts/script.sh
new file mode 100644
index 0000000..615bbb2
--- /dev/null
+++ b/testing/scenarios/custom_todos/scripts/script.sh
@@ -0,0 +1,15 @@
+# This is a malformed TODO
+# This is a normal comment
+curl "localhost:8080" # This is a TODO comment at the end of the line
+
+# This is a malformed ToDo
+# This is a normal comment
+curl "localhost:8080" # This is a ToDo comment at the end of the line
+
+# This is a malformed @fixme
+# This is a normal comment
+curl "localhost:8080" # This is a @fixme comment at the end of the line
+
+# This is a malformed @fix
+# This is a normal comment
+curl "localhost:8080" # This is a @fix comment at the end of the line
diff --git a/testing/scenarios/custom_todos/standard/main.go b/testing/scenarios/custom_todos/standard/main.go
new file mode 100644
index 0000000..2b67b3d
--- /dev/null
+++ b/testing/scenarios/custom_todos/standard/main.go
@@ -0,0 +1,29 @@
+package main
+
+// A malformed TODO comment
+// TODO 123: This is a valid todo comment
+// TODO 321: This is an invalid todo, marked against a closed issue
+/*
+ * TODO 567: This is an invalid multiline todo, marked against a non-existent issue
+ */
+
+// A malformed ToDo comment
+// ToDo 123: This is a valid todo comment
+// ToDo 321: This is an invalid todo, marked against a closed issue
+/*
+ * ToDo 567: This is an invalid multiline todo, marked against a non-existent issue
+ */
+
+// A malformed @fixme comment
+// @fixme 123: This is a valid todo comment
+// @fixme 321: This is an invalid todo, marked against a closed issue
+/*
+ * @fixme 567: This is an invalid multiline todo, marked against a non-existent issue
+ */
+
+// A malformed @fix comment
+// @fix 123: This is a valid todo comment
+// @fix 321: This is an invalid todo, marked against a closed issue
+/*
+ * @fix 567: This is an invalid multiline todo, marked against a non-existent issue
+ */
diff --git a/testing/scenarios/custom_todos/swift/main.swift b/testing/scenarios/custom_todos/swift/main.swift
new file mode 100644
index 0000000..53fa221
--- /dev/null
+++ b/testing/scenarios/custom_todos/swift/main.swift
@@ -0,0 +1,51 @@
+// Hello, World! Program
+import Swift
+
+print("Hello, World!") // TODO: An invalid todo
+
+/*
+ * TODO: An invalid multi-line todo
+ */
+
+/// TODO: An invalid docstring todo
+
+/*
+ /* TODO: An invalid nested todo */
+*/
+
+/*
+ /*
+    /* TODO 1: A valid nested todo */
+ */
+*/
+
+/*
+ /*
+    /* TODO 2: invalid todo as issue is closed */
+ */
+*/
+
+
+print("Hello, World!") // @fix: An invalid todo
+
+/*
+ * @fix: An invalid multi-line todo
+ */
+
+/// @fix: An invalid docstring todo
+
+/*
+ /* @fix: An invalid nested todo */
+*/
+
+/*
+ /*
+    /* @fix 1: A valid nested todo */
+ */
+*/
+
+/*
+ /*
+    /* @fix 2: invalid todo as issue is closed */
+ */
+*/
diff --git a/testing/scenarios/integrations/pivotaltracker/main.go b/testing/scenarios/integrations/pivotaltracker/main.go
new file mode 100644
index 0000000..699be76
--- /dev/null
+++ b/testing/scenarios/integrations/pivotaltracker/main.go
@@ -0,0 +1,15 @@
+package main
+
+// TODO #175938845: A started todo (open)
+
+// TODO #175938853: A finished todo (closed)
+
+// TODO #175938860: A delivered todo (closed)
+
+// TODO #175938836: An unstarted todo (open)
+
+// TODO #175938899: A rejected todo (closed)
+
+// TODO #175938883: An accepted todo (closed)
+
+// TODO #199938883: A non-existent issue
diff --git a/testing/scenarios/multiple_matchers/file1.sh b/testing/scenarios/multiple_matchers/file1.sh
new file mode 100644
index 0000000..8cc7d5b
--- /dev/null
+++ b/testing/scenarios/multiple_matchers/file1.sh
@@ -0,0 +1,1 @@
+# This is a malformed TODO with first matcher
diff --git a/testing/scenarios/multiple_matchers/file2.sh b/testing/scenarios/multiple_matchers/file2.sh
new file mode 100644
index 0000000..2b95e09
--- /dev/null
+++ b/testing/scenarios/multiple_matchers/file2.sh
@@ -0,0 +1,1 @@
+# This is a malformed TODO with first matcher in second file
diff --git a/testing/scenarios/multiple_matchers/file3.go b/testing/scenarios/multiple_matchers/file3.go
new file mode 100644
index 0000000..ab874dd
--- /dev/null
+++ b/testing/scenarios/multiple_matchers/file3.go
@@ -0,0 +1,3 @@
+package main
+
+// TODO: This is a todo in a file of other type with different matcher
diff --git a/testing/test_configs/integrations/pivotaltracker.yaml b/testing/test_configs/integrations/pivotaltracker.yaml
new file mode 100644
index 0000000..a4e0077
--- /dev/null
+++ b/testing/test_configs/integrations/pivotaltracker.yaml
@@ -0,0 +1,4 @@
+origin: https://www.pivotaltracker.com/n/projects/2459511
+issue_tracker: PIVOTAL_TRACKER
+auth:
+  type: apitoken
diff --git a/testing/test_configs/no_issue_tracker_and_custom_todos.yaml b/testing/test_configs/no_issue_tracker_and_custom_todos.yaml
new file mode 100644
index 0000000..f2d8ac1
--- /dev/null
+++ b/testing/test_configs/no_issue_tracker_and_custom_todos.yaml
@@ -0,0 +1,8 @@
+origin: http://myjira.com
+issue_tracker: JIRA
+auth:
+  type: none
+custom_todos:
+  - ToDo
+  - \@fixme
+  - "@fix"
diff --git a/testing/todocheck_test.go b/testing/todocheck_test.go
index e2da3f3..95aa1b6 100644
--- a/testing/todocheck_test.go
+++ b/testing/todocheck_test.go
@@ -802,3 +802,31 @@ func TestPrintingVersionFlagStopsProgram(t *testing.T) {
 		t.Errorf("%s", err)
 	}
 }
+
+// Testing multiple todo matchers created for different file types
+func TestMultipleTodoMatchers(t *testing.T) {
+	err := scenariobuilder.NewScenario().
+		WithBinary("../todocheck").
+		WithBasepath("./scenarios/multiple_matchers").
+		WithConfig("./test_configs/no_issue_tracker.yaml").
+		WithIssueTracker(issuetracker.Jira).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/multiple_matchers/file1.sh", 1).
+				ExpectLine("# This is a malformed TODO with first matcher")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/multiple_matchers/file2.sh", 1).
+				ExpectLine("# This is a malformed TODO with first matcher in second file")).
+		ExpectTodoErr(
+			scenariobuilder.NewTodoErr().
+				WithType(errors.TODOErrTypeMalformed).
+				WithLocation("scenarios/multiple_matchers/file3.go", 3).
+				ExpectLine("// TODO: This is a todo in a file of other type with different matcher")).
+		Run()
+	if err != nil {
+		t.Errorf("%s", err)
+	}
+}