These guidelines define the quality checks every developer must pass before committing code to this repository. They are tailored to this codebase's architecture, patterns, and tooling. For general contribution setup, see CONTRIBUTING.md.
When to use this: Before every git commit. Treat each section as a self-review checklist.
- Required Local Checks
- Architecture Adherence
- Interface Design
- Error Handling
- Testing Standards
- Concurrency Patterns
- Security
- Performance
- Code Style and Linting Thresholds
- Naming Conventions
- Common Mistakes to Avoid
- Pre-Commit Quick Reference
Do NOT commit if any of these fail. Run them locally before every commit:
# 1. Lint — must pass with zero issues
make lint
# 2. Tests — must pass with race detector enabled
make test
# 3. Coverage — must meet the 80% project-wide threshold
make coverage-check| Check | Command | What it validates |
|---|---|---|
| Linting | make lint |
golangci-lint v2 with 30+ linters (pinned version from .golangci-lint-version) |
| Tests | make test |
All tests with -race flag, -v for verbose output |
| Coverage | make coverage-check |
80% project-wide threshold (configured in .testcoverage.yml) |
# Install the pinned golangci-lint version
make lint-install
# Install the coverage threshold checker
go install github.com/vladopajic/go-test-coverage/v2@latestCoverage excludes cmd/ and internal/cli/ (CLI boilerplate) and generated files (*.pb.go, *_generated.go). See .testcoverage.yml for the full exclusion list.
- Lint failure: Fix the issue. Do not add
//nolintwithout a specific linter and explanation. - Test failure: Investigate the root cause. Do not skip or comment out tests.
- Coverage failure: Add tests for your new code. The threshold is 80% project-wide.
The project follows a hexagonal (clean) architecture. Dependencies flow inward:
cli/ --> engine/ --> scanner/, rules/, dependency/, language/ --> entities/
| |
| cache/, api/, output/ (infrastructure)
v
cmd/crypto-finder/main.go (calls only cli.Execute())
- Your code respects layer boundaries — no upward imports (e.g.,
scanner/must never importengine/orcli/). - Adapter implementations (
scanner/opengrep/,scanner/semgrep/,dependency/) depend on their interface package, not on concrete siblings. -
entities/has zero internal imports — it is the innermost layer. Adding an import here breaks the architecture. - New scanning logic goes through
engine.Orchestrator, not directly incli/commands. -
cli/commands only: validate input, wire dependencies, callengine/orscan/, handle output. No business logic in Cobra command functions. - All packages remain under
internal/— this project exposes no public Go API. - New packages include a package-level doc comment explaining their responsibility.
Reference implementations: scanner.Scanner (internal/scanner/interface.go), rules.RuleSource (internal/rules/source.go), dependency.Resolver (internal/dependency/resolver.go), language.Detector (internal/language/detector.go).
- Interfaces are defined at the consumer package, not the provider. The
scanner.Scannerinterface lives inscanner/, not inscanner/opengrep/. - Your interface has 2-4 methods. More than 5 is a design smell — consider splitting.
- New implementations register with the appropriate registry (
scanner.Registry,dependency.Registry). - You did not create an interface for a single concrete implementation unless testing requires it. Prefer inline struct mocks.
- Optional configuration uses the functional options pattern (see
callgraph.ParserOptiononparserConfig). Avoid builder chains or config structs with many boolean fields.
Reference: internal/errors/formatter.go for CLI-facing error utilities.
- Always wrap with context:
fmt.Errorf("failed to <verb> <noun>: %w", err). Describe WHAT failed. - Use
%w(not%vor%s) to preserve the error chain. - Use
errors.Is()/errors.As()instead of direct==comparison (errorlintenforces this). - CLI-facing errors use utilities from
internal/errors/:FormatError,FormatScannerError,FormatValidationError,WrapWithSuggestion,FormatMultiError. - Non-critical failures (cache misses, graph enrichment, optional API calls) log warnings via zerolog and continue. Only return errors that should stop the pipeline.
- Type assertions must use the
okpattern (errcheckhascheck-type-assertions: true).
// BAD: swallowing error
_ = file.Close()
// BAD: direct comparison (breaks with wrapping)
if err == os.ErrNotExist { ... }
// BAD: wrapping without context
return fmt.Errorf("%w", err)
// BAD: %v breaks error chain
return fmt.Errorf("scan failed: %v", err)
// GOOD:
return fmt.Errorf("failed to close report file: %w", err)
if errors.Is(err, os.ErrNotExist) { ... }- All errors are wrapped with
%wand descriptive context. -
errors.Is()/errors.As()used instead of==comparison. - Type assertions use the
okpattern. - CLI-facing errors use
internal/errors/utilities. - Non-critical failures log warnings but don't fail the pipeline.
Reference: internal/engine/orchestrator_test.go, internal/converter/primitives_test.go, internal/cache/manager_test.go.
t.Parallel()on every test function AND everyt.Run()subtest. No exceptions.- Inline struct mocks with func fields (e.g.,
mockRuleSource { loadFunc func() },stubParser { parseFunc func() }). Do NOT addgomock,mockgen,testify/mock, or any code-gen mocking library to this project. - Table-driven tests with
tests := []struct{ name string; ... }andttas the loop variable (nottcortest). t.TempDir()for any filesystem operations. Never write to fixed paths or leave test artifacts.t.Helper()on every test helper function that callst.Fatal,t.Error, or similar.- Integration tests go in
*_integration_test.gofiles and must guard witht.Skip()when external tools are unavailable (seecheckOpengrepAvailablepattern). - Assertions: Use
requirefrom testify for fatal preconditions (setup failures),assertfor non-fatal verifications. - Coverage: Your code must maintain the 80% project-wide threshold. Run
make coverage-check. - Test files are excluded from complexity linters (
gocognit,gocyclo,funlen,goconst,gosec,errcheck).
- Every test function and subtest calls
t.Parallel(). - Mocks are inline struct types with func fields — no external mock libraries.
- Table-driven tests use
ttas the loop variable. -
t.TempDir()used for filesystem operations. -
t.Helper()on every test helper function. - Integration tests guard with
t.Skip()when tools are unavailable. -
requirefor fatal preconditions,assertfor non-fatal checks. - Coverage maintained at >= 80% (
make coverage-check).
Reference: internal/engine/dependency_scanner.go (worker pool), internal/config/config.go (singleton with sync.Once), internal/scanner/registry.go (thread-safe registry).
context.Contextas first parameter on all blocking or long-running operations. Thenoctxlinter catches HTTP requests missing context.sync.RWMutex: UseRLock()for read-only operations,Lock()for mutations. Do not useLock()for everything.- Worker pool pattern: Buffered channels sized to
len(items),sync.WaitGroupfor tracking, capped atmaxWorkers = 8. SeescanDependenciesParallelfor the canonical implementation. - All goroutines tracked via
sync.WaitGroupor equivalent. No fire-and-forget goroutines. exec.CommandContextfor external process execution — ensures cancellation kills the process.
// BAD: unbuffered channel with multiple senders (deadlock risk)
ch := make(chan result)
// BAD: missing context
func (s *Scanner) Scan(target string, ...) // should accept ctx
// BAD: Lock() for read-only operation
r.mu.Lock() // should be r.mu.RLock() if only reading
// BAD: untracked goroutine
go func() { process(item) }() // leaked goroutine-
context.Contextis the first parameter on all blocking operations. -
RLock()for reads,Lock()for writes. - Worker pools use buffered channels sized to input length.
- All goroutines tracked via
sync.WaitGroup. - Worker count respects the
maxWorkerscap.
- File permissions:
0o600for sensitive files (config, API keys),0o750for directories. Never0o777or0o666. - API key handling: Keys come from
config.GetAPIKey()orSCANOSS_API_KEY. Never log API keys. Never include them in error messages. Reference:x-api-keyheader ininternal/api/. - Command execution: Scanner adapters run external tools. Use
exec.CommandContextwith timeout. Verify command arguments are not constructed from untrusted user input without validation. - Atomic file writes: For crash-safe operations, follow the pattern in
DiskFindingsCache.Put: write to temp file, sync, close, rename. - gosec exclusions: G204 (command execution) and G304 (file path taint) are excluded from automated linting. You must self-review these patterns manually before committing.
- GPL-2.0-only headers: Every new
.gofile must include the full copyright header. SeeCONTRIBUTING.mdfor the exact format.
- File permissions:
0o600for sensitive files,0o750for directories. - No API keys or secrets in log messages or error strings.
-
exec.CommandContextused with timeout for external processes. - User-controlled paths validated before use (self-review for G304).
- Command arguments validated before execution (self-review for G204).
- Atomic write pattern used for crash-safe file operations.
- GPL-2.0-only header present on all new
.gofiles.
- Slice pre-allocation: Use
make([]T, 0, expectedLen)when capacity is known. Theprealloclinter catches simple cases, range loops, and for loops. - HTTP response body close:
bodycloselinter is enabled. Alwaysdefer resp.Body.Close()immediately after the error check. - Cache-first: Expensive operations (dependency resolution, bytecode analysis, rule fetching) must check cache before computing. Reference:
FindingsCacheandcache.Manager. - Large structs by pointer:
gocriticflags structs over 256 bytes. Pass them by pointer, use pointer receivers. - Rule filtering: The orchestrator filters rules by detected language BEFORE passing them to the scanner. New scanner workflows must include this optimization.
- No unnecessary type conversions: The
unconvertlinter catches these.
- Slices pre-allocated when capacity is known.
- HTTP response bodies always closed.
- Expensive operations check cache before computing.
- Large structs (>256 bytes) passed by pointer.
- Rule sets filtered by language before scanner invocation.
All thresholds are configured in .golangci.yml. These are the hard numbers — make lint enforces them:
| Metric | Threshold | Scope |
|---|---|---|
| Function length | 120 lines / 60 statements | Production code (excluded in tests and cmd/) |
| Cyclomatic complexity | 15 | Production code (excluded in tests) |
| Cognitive complexity | 20 | Production code (excluded in tests and cmd/) |
| Nesting depth | 5 | Production code |
| String repetition | 3+ occurrences, 3+ chars | Production code (excluded in tests) |
| Huge parameter | 256 bytes | All code |
Three groups, enforced by goimports with local prefix github.com/scanoss/crypto-finder:
import (
// Standard library
"context"
"fmt"
// External dependencies
"github.com/rs/zerolog/log"
// Internal packages
"github.com/scanoss/crypto-finder/internal/entities"
"github.com/scanoss/crypto-finder/internal/scanner"
)Every //nolint directive MUST specify the exact linter AND include an explanation. The nolintlint linter enforces this. If you need to suppress a warning, justify it:
// GOOD:
//nolint:gosec // G304 -- path is validated by validateTargetPath() before this call.
// BAD — will fail lint:
//nolint
//nolint:gosec- Comments end in a period. Exception:
// TODOcomments (godotlinter). context.Contextis always the first parameter (revive: context-as-argument).erroris always the last return value (revive: error-return).- Early returns for errors — no
elseafter a return (revive: indent-error-flow).
-
make lintpasses with zero issues. - Functions under 120 lines / 60 statements.
- Cyclomatic complexity under 15, cognitive complexity under 20.
- Nesting depth under 5.
-
//nolintdirectives specify the linter + an explanation. - Import groups: stdlib | external | internal.
- Comments end in a period (except
// TODO).
These conventions are derived from existing codebase patterns:
| Element | Convention | Examples |
|---|---|---|
| Packages | Lowercase, single word | scanner, rules, cache, entities |
| Sub-packages | Implementation-specific | scanner/opengrep, scanner/semgrep |
| Interfaces | Capability/role names | Scanner, Detector, Resolver, Writer |
| Constructors | NewXxx() |
NewOrchestrator(), NewRegistry() |
| Alt constructors | NewXxxWithYyy() |
NewDiskFindingsCacheWithDir() |
| Config structs | Suffix Options |
ScanOptions, DepScanOptions |
| Functional options | Suffix Option |
ParserOption |
| Status enums | Unexported, iota |
depScanStatus with statusPending, statusDone |
| Test files | Co-located *_test.go |
orchestrator_test.go alongside orchestrator.go |
| Integration tests | *_integration_test.go |
scanner_integration_test.go |
Do NOT use IScanner, ScannerInterface, or Hungarian notation.
Self-check for these codebase-specific issues before committing:
-
Direct
config.GetInstance()in domain logic. The config singleton should only be accessed incli/for wiring. Deeper packages receive configuration through constructor parameters (dependency injection). -
Importing
cli/from non-CLI packages. Thecli/package is the outermost layer. Nothing inengine/,scanner/,entities/, or any other package should import it. -
Adding external mock libraries. This project uses inline struct mocks with func fields. Do not add
gomock,mockgen,testify/mock, or similar. -
Logging to stdout. All logging goes to stderr via zerolog. Stdout is reserved for program output (JSON reports, CycloneDX). Mixing these breaks piping.
-
Ignoring context cancellation. Scanner adapters execute external processes. If
ctxis cancelled, the process must be killed. Always useexec.CommandContext. -
Creating new singletons. The project has ONE singleton (
config.Config). New singletons are an anti-pattern. Use dependency injection. -
Business logic in Cobra commands. Commands validate input, wire dependencies, call
engine/, and handle output. That's it. -
Breaking
entities/purity. Theentities/package must not import any otherinternal/package. -
Hardcoding file paths. Use
config.GetCacheDir(),t.TempDir(), oros.UserHomeDir(). Never hardcode/home/,/tmp/, or absolute paths. -
Unbounded goroutine creation. Parallel work must respect the
maxWorkerscap. Spawning unbounded goroutines will overwhelm the system with scanner subprocesses.
Run through this before every git commit:
make lint # Zero lint issues
make test # All tests pass with -race
make coverage-check # 80% coverage threshold met-
make lintpasses. -
make testpasses (with-race). -
make coverage-checkpasses (80% threshold). - No upward layer imports — architecture boundaries respected.
-
entities/has zero internal imports. - Errors wrapped with
%wand descriptive context. -
t.Parallel()on all tests and subtests. - Inline struct mocks only — no external mock libraries.
-
t.TempDir()for filesystem operations in tests. - GPL-2.0-only header on all new
.gofiles. - No secrets in logs or error messages.
-
exec.CommandContextfor external processes. - CHANGELOG.md updated under "Unreleased".