v1.4.9: Validation fixes, Profiler fix, Quest link duplicate fix, Ascension fixes
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# Rules
|
||||
## Structure
|
||||
|
||||
Rules are organized into a **common** layer plus **language-specific** directories:
|
||||
|
||||
```
|
||||
rules/
|
||||
├── common/ # Language-agnostic principles (always install)
|
||||
│ ├── coding-style.md
|
||||
│ ├── git-workflow.md
|
||||
│ ├── testing.md
|
||||
│ ├── performance.md
|
||||
│ ├── patterns.md
|
||||
│ ├── hooks.md
|
||||
│ ├── agents.md
|
||||
│ └── security.md
|
||||
├── typescript/ # TypeScript/JavaScript specific
|
||||
├── python/ # Python specific
|
||||
├── golang/ # Go specific
|
||||
├── swift/ # Swift specific
|
||||
└── php/ # PHP specific
|
||||
```
|
||||
|
||||
- **common/** contains universal principles — no language-specific code examples.
|
||||
- **Language directories** extend the common rules with framework-specific patterns, tools, and code examples. Each file references its common counterpart.
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Install Script (Recommended)
|
||||
|
||||
```bash
|
||||
# Install common + one or more language-specific rule sets
|
||||
./install.sh typescript
|
||||
./install.sh python
|
||||
./install.sh golang
|
||||
./install.sh swift
|
||||
./install.sh php
|
||||
|
||||
# Install multiple languages at once
|
||||
./install.sh typescript python
|
||||
```
|
||||
|
||||
### Option 2: Manual Installation
|
||||
|
||||
> **Important:** Copy entire directories — do NOT flatten with `/*`.
|
||||
> Common and language-specific directories contain files with the same names.
|
||||
> Flattening them into one directory causes language-specific files to overwrite
|
||||
> common rules, and breaks the relative `../common/` references used by
|
||||
> language-specific files.
|
||||
|
||||
```bash
|
||||
# Install common rules (required for all projects)
|
||||
cp -r rules/common ~/.claude/rules/common
|
||||
|
||||
# Install language-specific rules based on your project's tech stack
|
||||
cp -r rules/typescript ~/.claude/rules/typescript
|
||||
cp -r rules/python ~/.claude/rules/python
|
||||
cp -r rules/golang ~/.claude/rules/golang
|
||||
cp -r rules/swift ~/.claude/rules/swift
|
||||
cp -r rules/php ~/.claude/rules/php
|
||||
|
||||
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
|
||||
```
|
||||
|
||||
## Rules vs Skills
|
||||
|
||||
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
|
||||
- **Skills** (`skills/` directory) provide deep, actionable reference material for specific tasks (e.g., `python-patterns`, `golang-testing`).
|
||||
|
||||
Language-specific rule files reference relevant skills where appropriate. Rules tell you *what* to do; skills tell you *how* to do it.
|
||||
|
||||
## Adding a New Language
|
||||
|
||||
To add support for a new language (e.g., `rust/`):
|
||||
|
||||
1. Create a `rules/rust/` directory
|
||||
2. Add files that extend the common rules:
|
||||
- `coding-style.md` — formatting tools, idioms, error handling patterns
|
||||
- `testing.md` — test framework, coverage tools, test organization
|
||||
- `patterns.md` — language-specific design patterns
|
||||
- `hooks.md` — PostToolUse hooks for formatters, linters, type checkers
|
||||
- `security.md` — secret management, security scanning tools
|
||||
3. Each file should start with:
|
||||
```
|
||||
> This file extends [common/xxx.md](../common/xxx.md) with <Language> specific content.
|
||||
```
|
||||
4. Reference existing skills if available, or create new ones under `skills/`.
|
||||
|
||||
## Rule Priority
|
||||
|
||||
When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence).
|
||||
|
||||
- `rules/common/` defines universal defaults applicable to all projects.
|
||||
- `rules/golang/`, `rules/python/`, `rules/swift/`, `rules/php/`, `rules/typescript/`, etc. override those defaults where language idioms differ.
|
||||
|
||||
### Example
|
||||
|
||||
`common/coding-style.md` recommends immutability as a default principle. A language-specific `golang/coding-style.md` can override this:
|
||||
|
||||
> Idiomatic Go uses pointer receivers for struct mutation — see [common/coding-style.md](../common/coding-style.md) for the general principle, but Go-idiomatic mutation is preferred here.
|
||||
|
||||
### Common rules with override notes
|
||||
|
||||
Rules in `rules/common/` that may be overridden by language-specific files are marked with:
|
||||
|
||||
> **Language note**: This rule may be overridden by language-specific rules for languages where this pattern is not idiomatic.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Agent Orchestration
|
||||
|
||||
## Available Agents
|
||||
|
||||
Located in `~/.claude/agents/`:
|
||||
|
||||
| Agent | Purpose | When to Use |
|
||||
|-------|---------|-------------|
|
||||
| planner | Implementation planning | Complex features, refactoring |
|
||||
| architect | System design | Architectural decisions |
|
||||
| tdd-guide | Test-driven development | New features, bug fixes |
|
||||
| code-reviewer | Code review | After writing code |
|
||||
| security-reviewer | Security analysis | Before commits |
|
||||
| build-error-resolver | Fix build errors | When build fails |
|
||||
| e2e-runner | E2E testing | Critical user flows |
|
||||
| refactor-cleaner | Dead code cleanup | Code maintenance |
|
||||
| doc-updater | Documentation | Updating docs |
|
||||
| rust-reviewer | Rust code review | Rust projects |
|
||||
|
||||
## Immediate Agent Usage
|
||||
|
||||
No user prompt needed:
|
||||
1. Complex feature requests - Use **planner** agent
|
||||
2. Code just written/modified - Use **code-reviewer** agent
|
||||
3. Bug fix or new feature - Use **tdd-guide** agent
|
||||
4. Architectural decision - Use **architect** agent
|
||||
|
||||
## Parallel Task Execution
|
||||
|
||||
ALWAYS use parallel Task execution for independent operations:
|
||||
|
||||
```markdown
|
||||
# GOOD: Parallel execution
|
||||
Launch 3 agents in parallel:
|
||||
1. Agent 1: Security analysis of auth module
|
||||
2. Agent 2: Performance review of cache system
|
||||
3. Agent 3: Type checking of utilities
|
||||
|
||||
# BAD: Sequential when unnecessary
|
||||
First agent 1, then agent 2, then agent 3
|
||||
```
|
||||
|
||||
## Multi-Perspective Analysis
|
||||
|
||||
For complex problems, use split role sub-agents:
|
||||
- Factual reviewer
|
||||
- Senior engineer
|
||||
- Security expert
|
||||
- Consistency reviewer
|
||||
- Redundancy checker
|
||||
@@ -0,0 +1,48 @@
|
||||
# Coding Style
|
||||
|
||||
## Immutability (CRITICAL)
|
||||
|
||||
ALWAYS create new objects, NEVER mutate existing ones:
|
||||
|
||||
```
|
||||
// Pseudocode
|
||||
WRONG: modify(original, field, value) → changes original in-place
|
||||
CORRECT: update(original, field, value) → returns new copy with change
|
||||
```
|
||||
|
||||
Rationale: Immutable data prevents hidden side effects, makes debugging easier, and enables safe concurrency.
|
||||
|
||||
## File Organization
|
||||
|
||||
MANY SMALL FILES > FEW LARGE FILES:
|
||||
- High cohesion, low coupling
|
||||
- 200-400 lines typical, 800 max
|
||||
- Extract utilities from large modules
|
||||
- Organize by feature/domain, not by type
|
||||
|
||||
## Error Handling
|
||||
|
||||
ALWAYS handle errors comprehensively:
|
||||
- Handle errors explicitly at every level
|
||||
- Provide user-friendly error messages in UI-facing code
|
||||
- Log detailed error context on the server side
|
||||
- Never silently swallow errors
|
||||
|
||||
## Input Validation
|
||||
|
||||
ALWAYS validate at system boundaries:
|
||||
- Validate all user input before processing
|
||||
- Use schema-based validation where available
|
||||
- Fail fast with clear error messages
|
||||
- Never trust external data (API responses, user input, file content)
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
Before marking work complete:
|
||||
- [ ] Code is readable and well-named
|
||||
- [ ] Functions are small (<50 lines)
|
||||
- [ ] Files are focused (<800 lines)
|
||||
- [ ] No deep nesting (>4 levels)
|
||||
- [ ] Proper error handling
|
||||
- [ ] No hardcoded values (use constants or config)
|
||||
- [ ] No mutation (immutable patterns used)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Development Workflow
|
||||
|
||||
> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations.
|
||||
|
||||
The Feature Implementation Workflow describes the development pipeline: research, planning, TDD, code review, and then committing to git.
|
||||
|
||||
## Feature Implementation Workflow
|
||||
|
||||
0. **Research & Reuse** _(mandatory before any new implementation)_
|
||||
- **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new.
|
||||
- **Library docs second:** Use Context7 or primary vendor docs to confirm API behavior, package usage, and version-specific details before implementing.
|
||||
- **Exa only when the first two are insufficient:** Use Exa for broader web research or discovery after GitHub search and primary docs.
|
||||
- **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions.
|
||||
- **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped.
|
||||
- Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement.
|
||||
|
||||
1. **Plan First**
|
||||
- Use **planner** agent to create implementation plan
|
||||
- Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list
|
||||
- Identify dependencies and risks
|
||||
- Break down into phases
|
||||
|
||||
2. **TDD Approach**
|
||||
- Use **tdd-guide** agent
|
||||
- Write tests first (RED)
|
||||
- Implement to pass tests (GREEN)
|
||||
- Refactor (IMPROVE)
|
||||
- Verify 80%+ coverage
|
||||
|
||||
3. **Code Review**
|
||||
- Use **code-reviewer** agent immediately after writing code
|
||||
- Address CRITICAL and HIGH issues
|
||||
- Fix MEDIUM issues when possible
|
||||
|
||||
4. **Commit & Push**
|
||||
- Detailed commit messages
|
||||
- Follow conventional commits format
|
||||
- See [git-workflow.md](./git-workflow.md) for commit message format and PR process
|
||||
@@ -0,0 +1,24 @@
|
||||
# Git Workflow
|
||||
|
||||
## Commit Message Format
|
||||
```
|
||||
<type>: <description>
|
||||
|
||||
<optional body>
|
||||
```
|
||||
|
||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Note: Attribution disabled globally via ~/.claude/settings.json.
|
||||
|
||||
## Pull Request Workflow
|
||||
|
||||
When creating PRs:
|
||||
1. Analyze full commit history (not just latest commit)
|
||||
2. Use `git diff [base-branch]...HEAD` to see all changes
|
||||
3. Draft comprehensive PR summary
|
||||
4. Include test plan with TODOs
|
||||
5. Push with `-u` flag if new branch
|
||||
|
||||
> For the full development process (planning, TDD, code review) before git operations,
|
||||
> see [development-workflow.md](./development-workflow.md).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Hooks System
|
||||
|
||||
## Hook Types
|
||||
|
||||
- **PreToolUse**: Before tool execution (validation, parameter modification)
|
||||
- **PostToolUse**: After tool execution (auto-format, checks)
|
||||
- **Stop**: When session ends (final verification)
|
||||
|
||||
## Auto-Accept Permissions
|
||||
|
||||
Use with caution:
|
||||
- Enable for trusted, well-defined plans
|
||||
- Disable for exploratory work
|
||||
- Never use dangerously-skip-permissions flag
|
||||
- Configure `allowedTools` in `~/.claude.json` instead
|
||||
|
||||
## TodoWrite Best Practices
|
||||
|
||||
Use TodoWrite tool to:
|
||||
- Track progress on multi-step tasks
|
||||
- Verify understanding of instructions
|
||||
- Enable real-time steering
|
||||
- Show granular implementation steps
|
||||
|
||||
Todo list reveals:
|
||||
- Out of order steps
|
||||
- Missing items
|
||||
- Extra unnecessary items
|
||||
- Wrong granularity
|
||||
- Misinterpreted requirements
|
||||
@@ -0,0 +1,31 @@
|
||||
# Common Patterns
|
||||
|
||||
## Skeleton Projects
|
||||
|
||||
When implementing new functionality:
|
||||
1. Search for battle-tested skeleton projects
|
||||
2. Use parallel agents to evaluate options:
|
||||
- Security assessment
|
||||
- Extensibility analysis
|
||||
- Relevance scoring
|
||||
- Implementation planning
|
||||
3. Clone best match as foundation
|
||||
4. Iterate within proven structure
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
Encapsulate data access behind a consistent interface:
|
||||
- Define standard operations: findAll, findById, create, update, delete
|
||||
- Concrete implementations handle storage details (database, API, file, etc.)
|
||||
- Business logic depends on the abstract interface, not the storage mechanism
|
||||
- Enables easy swapping of data sources and simplifies testing with mocks
|
||||
|
||||
### API Response Format
|
||||
|
||||
Use a consistent envelope for all API responses:
|
||||
- Include a success/status indicator
|
||||
- Include the data payload (nullable on error)
|
||||
- Include an error message field (nullable on success)
|
||||
- Include metadata for paginated responses (total, page, limit)
|
||||
@@ -0,0 +1,55 @@
|
||||
# Performance Optimization
|
||||
|
||||
## Model Selection Strategy
|
||||
|
||||
**Haiku 4.5** (90% of Sonnet capability, 3x cost savings):
|
||||
- Lightweight agents with frequent invocation
|
||||
- Pair programming and code generation
|
||||
- Worker agents in multi-agent systems
|
||||
|
||||
**Sonnet 4.6** (Best coding model):
|
||||
- Main development work
|
||||
- Orchestrating multi-agent workflows
|
||||
- Complex coding tasks
|
||||
|
||||
**Opus 4.5** (Deepest reasoning):
|
||||
- Complex architectural decisions
|
||||
- Maximum reasoning requirements
|
||||
- Research and analysis tasks
|
||||
|
||||
## Context Window Management
|
||||
|
||||
Avoid last 20% of context window for:
|
||||
- Large-scale refactoring
|
||||
- Feature implementation spanning multiple files
|
||||
- Debugging complex interactions
|
||||
|
||||
Lower context sensitivity tasks:
|
||||
- Single-file edits
|
||||
- Independent utility creation
|
||||
- Documentation updates
|
||||
- Simple bug fixes
|
||||
|
||||
## Extended Thinking + Plan Mode
|
||||
|
||||
Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning.
|
||||
|
||||
Control extended thinking via:
|
||||
- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux)
|
||||
- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json`
|
||||
- **Budget cap**: `export MAX_THINKING_TOKENS=10000`
|
||||
- **Verbose mode**: Ctrl+O to see thinking output
|
||||
|
||||
For complex tasks requiring deep reasoning:
|
||||
1. Ensure extended thinking is enabled (on by default)
|
||||
2. Enable **Plan Mode** for structured approach
|
||||
3. Use multiple critique rounds for thorough analysis
|
||||
4. Use split role sub-agents for diverse perspectives
|
||||
|
||||
## Build Troubleshooting
|
||||
|
||||
If build fails:
|
||||
1. Use **build-error-resolver** agent
|
||||
2. Analyze error messages
|
||||
3. Fix incrementally
|
||||
4. Verify after each fix
|
||||
@@ -0,0 +1,29 @@
|
||||
# Security Guidelines
|
||||
|
||||
## Mandatory Security Checks
|
||||
|
||||
Before ANY commit:
|
||||
- [ ] No hardcoded secrets (API keys, passwords, tokens)
|
||||
- [ ] All user inputs validated
|
||||
- [ ] SQL injection prevention (parameterized queries)
|
||||
- [ ] XSS prevention (sanitized HTML)
|
||||
- [ ] CSRF protection enabled
|
||||
- [ ] Authentication/authorization verified
|
||||
- [ ] Rate limiting on all endpoints
|
||||
- [ ] Error messages don't leak sensitive data
|
||||
|
||||
## Secret Management
|
||||
|
||||
- NEVER hardcode secrets in source code
|
||||
- ALWAYS use environment variables or a secret manager
|
||||
- Validate that required secrets are present at startup
|
||||
- Rotate any secrets that may have been exposed
|
||||
|
||||
## Security Response Protocol
|
||||
|
||||
If security issue found:
|
||||
1. STOP immediately
|
||||
2. Use **security-reviewer** agent
|
||||
3. Fix CRITICAL issues before continuing
|
||||
4. Rotate any exposed secrets
|
||||
5. Review entire codebase for similar issues
|
||||
@@ -0,0 +1,29 @@
|
||||
# Testing Requirements
|
||||
|
||||
## Minimum Test Coverage: 80%
|
||||
|
||||
Test Types (ALL required):
|
||||
1. **Unit Tests** - Individual functions, utilities, components
|
||||
2. **Integration Tests** - API endpoints, database operations
|
||||
3. **E2E Tests** - Critical user flows (framework chosen per language)
|
||||
|
||||
## Test-Driven Development
|
||||
|
||||
MANDATORY workflow:
|
||||
1. Write test first (RED)
|
||||
2. Run test - it should FAIL
|
||||
3. Write minimal implementation (GREEN)
|
||||
4. Run test - it should PASS
|
||||
5. Refactor (IMPROVE)
|
||||
6. Verify coverage (80%+)
|
||||
|
||||
## Troubleshooting Test Failures
|
||||
|
||||
1. Use **tdd-guide** agent
|
||||
2. Check test isolation
|
||||
3. Verify mocks are correct
|
||||
4. Fix implementation, not tests (unless tests are wrong)
|
||||
|
||||
## Agent Support
|
||||
|
||||
- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with C++ specific content.
|
||||
|
||||
## Modern C++ (C++17/20/23)
|
||||
|
||||
- Prefer **modern C++ features** over C-style constructs
|
||||
- Use `auto` when the type is obvious from context
|
||||
- Use `constexpr` for compile-time constants
|
||||
- Use structured bindings: `auto [key, value] = map_entry;`
|
||||
|
||||
## Resource Management
|
||||
|
||||
- **RAII everywhere** — no manual `new`/`delete`
|
||||
- Use `std::unique_ptr` for exclusive ownership
|
||||
- Use `std::shared_ptr` only when shared ownership is truly needed
|
||||
- Use `std::make_unique` / `std::make_shared` over raw `new`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Types/Classes: `PascalCase`
|
||||
- Functions/Methods: `snake_case` or `camelCase` (follow project convention)
|
||||
- Constants: `kPascalCase` or `UPPER_SNAKE_CASE`
|
||||
- Namespaces: `lowercase`
|
||||
- Member variables: `snake_case_` (trailing underscore) or `m_` prefix
|
||||
|
||||
## Formatting
|
||||
|
||||
- Use **clang-format** — no style debates
|
||||
- Run `clang-format -i <file>` before committing
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `cpp-coding-standards` for comprehensive C++ coding standards and guidelines.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with C++ specific content.
|
||||
|
||||
## Build Hooks
|
||||
|
||||
Run these checks before committing C++ changes:
|
||||
|
||||
```bash
|
||||
# Format check
|
||||
clang-format --dry-run --Werror src/*.cpp src/*.hpp
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.cpp -- -std=c++17
|
||||
|
||||
# Build
|
||||
cmake --build build
|
||||
|
||||
# Tests
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## Recommended CI Pipeline
|
||||
|
||||
1. **clang-format** — formatting check
|
||||
2. **clang-tidy** — static analysis
|
||||
3. **cppcheck** — additional analysis
|
||||
4. **cmake build** — compilation
|
||||
5. **ctest** — test execution with sanitizers
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with C++ specific content.
|
||||
|
||||
## RAII (Resource Acquisition Is Initialization)
|
||||
|
||||
Tie resource lifetime to object lifetime:
|
||||
|
||||
```cpp
|
||||
class FileHandle {
|
||||
public:
|
||||
explicit FileHandle(const std::string& path) : file_(std::fopen(path.c_str(), "r")) {}
|
||||
~FileHandle() { if (file_) std::fclose(file_); }
|
||||
FileHandle(const FileHandle&) = delete;
|
||||
FileHandle& operator=(const FileHandle&) = delete;
|
||||
private:
|
||||
std::FILE* file_;
|
||||
};
|
||||
```
|
||||
|
||||
## Rule of Five/Zero
|
||||
|
||||
- **Rule of Zero**: Prefer classes that need no custom destructor, copy/move constructors, or assignments
|
||||
- **Rule of Five**: If you define any of destructor/copy-ctor/copy-assign/move-ctor/move-assign, define all five
|
||||
|
||||
## Value Semantics
|
||||
|
||||
- Pass small/trivial types by value
|
||||
- Pass large types by `const&`
|
||||
- Return by value (rely on RVO/NRVO)
|
||||
- Use move semantics for sink parameters
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Use exceptions for exceptional conditions
|
||||
- Use `std::optional` for values that may not exist
|
||||
- Use `std::expected` (C++23) or result types for expected failures
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `cpp-coding-standards` for comprehensive C++ patterns and anti-patterns.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with C++ specific content.
|
||||
|
||||
## Memory Safety
|
||||
|
||||
- Never use raw `new`/`delete` — use smart pointers
|
||||
- Never use C-style arrays — use `std::array` or `std::vector`
|
||||
- Never use `malloc`/`free` — use C++ allocation
|
||||
- Avoid `reinterpret_cast` unless absolutely necessary
|
||||
|
||||
## Buffer Overflows
|
||||
|
||||
- Use `std::string` over `char*`
|
||||
- Use `.at()` for bounds-checked access when safety matters
|
||||
- Never use `strcpy`, `strcat`, `sprintf` — use `std::string` or `fmt::format`
|
||||
|
||||
## Undefined Behavior
|
||||
|
||||
- Always initialize variables
|
||||
- Avoid signed integer overflow
|
||||
- Never dereference null or dangling pointers
|
||||
- Use sanitizers in CI:
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
|
||||
```
|
||||
|
||||
## Static Analysis
|
||||
|
||||
- Use **clang-tidy** for automated checks:
|
||||
```bash
|
||||
clang-tidy --checks='*' src/*.cpp
|
||||
```
|
||||
- Use **cppcheck** for additional analysis:
|
||||
```bash
|
||||
cppcheck --enable=all src/
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `cpp-coding-standards` for detailed security guidelines.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with C++ specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use **GoogleTest** (gtest/gmock) with **CMake/CTest**.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
cmake --build build && ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" ..
|
||||
cmake --build .
|
||||
ctest --output-on-failure
|
||||
lcov --capture --directory . --output-file coverage.info
|
||||
```
|
||||
|
||||
## Sanitizers
|
||||
|
||||
Always run tests with sanitizers in CI:
|
||||
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `cpp-testing` for detailed C++ testing patterns, TDD workflow, and GoogleTest/GMock usage.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
---
|
||||
# C# Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with C#-specific content.
|
||||
|
||||
## Standards
|
||||
|
||||
- Follow current .NET conventions and enable nullable reference types
|
||||
- Prefer explicit access modifiers on public and internal APIs
|
||||
- Keep files aligned with the primary type they define
|
||||
|
||||
## Types and Models
|
||||
|
||||
- Prefer `record` or `record struct` for immutable value-like models
|
||||
- Use `class` for entities or types with identity and lifecycle
|
||||
- Use `interface` for service boundaries and abstractions
|
||||
- Avoid `dynamic` in application code; prefer generics or explicit models
|
||||
|
||||
```csharp
|
||||
public sealed record UserDto(Guid Id, string Email);
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<UserDto?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer `init` setters, constructor parameters, and immutable collections for shared state
|
||||
- Do not mutate input models in-place when producing updated state
|
||||
|
||||
```csharp
|
||||
public sealed record UserProfile(string Name, string Email);
|
||||
|
||||
public static UserProfile Rename(UserProfile profile, string name) =>
|
||||
profile with { Name = name };
|
||||
```
|
||||
|
||||
## Async and Error Handling
|
||||
|
||||
- Prefer `async`/`await` over blocking calls like `.Result` or `.Wait()`
|
||||
- Pass `CancellationToken` through public async APIs
|
||||
- Throw specific exceptions and log with structured properties
|
||||
|
||||
```csharp
|
||||
public async Task<Order> LoadOrderAsync(
|
||||
Guid orderId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await repository.FindAsync(orderId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Order {orderId} was not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to load order {OrderId}", orderId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
- Use `dotnet format` for formatting and analyzer fixes
|
||||
- Keep `using` directives organized and remove unused imports
|
||||
- Prefer expression-bodied members only when they stay readable
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
- "**/*.sln"
|
||||
- "**/Directory.Build.props"
|
||||
- "**/Directory.Build.targets"
|
||||
---
|
||||
# C# Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with C#-specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **dotnet format**: Auto-format edited C# files and apply analyzer fixes
|
||||
- **dotnet build**: Verify the solution or project still compiles after edits
|
||||
- **dotnet test --no-build**: Re-run the nearest relevant test project after behavior changes
|
||||
|
||||
## Stop Hooks
|
||||
|
||||
- Run a final `dotnet build` before ending a session with broad C# changes
|
||||
- Warn on modified `appsettings*.json` files so secrets do not get committed
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
---
|
||||
# C# Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with C#-specific content.
|
||||
|
||||
## API Response Pattern
|
||||
|
||||
```csharp
|
||||
public sealed record ApiResponse<T>(
|
||||
bool Success,
|
||||
T? Data = default,
|
||||
string? Error = null,
|
||||
object? Meta = null);
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```csharp
|
||||
public interface IRepository<T>
|
||||
{
|
||||
Task<IReadOnlyList<T>> FindAllAsync(CancellationToken cancellationToken);
|
||||
Task<T?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<T> CreateAsync(T entity, CancellationToken cancellationToken);
|
||||
Task<T> UpdateAsync(T entity, CancellationToken cancellationToken);
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## Options Pattern
|
||||
|
||||
Use strongly typed options for config instead of reading raw strings throughout the codebase.
|
||||
|
||||
```csharp
|
||||
public sealed class PaymentsOptions
|
||||
{
|
||||
public const string SectionName = "Payments";
|
||||
public required string BaseUrl { get; init; }
|
||||
public required string ApiKeySecretName { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
- Depend on interfaces at service boundaries
|
||||
- Keep constructors focused; if a service needs too many dependencies, split responsibilities
|
||||
- Register lifetimes intentionally: singleton for stateless/shared services, scoped for request data, transient for lightweight pure workers
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
- "**/appsettings*.json"
|
||||
---
|
||||
# C# Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with C#-specific content.
|
||||
|
||||
## Secret Management
|
||||
|
||||
- Never hardcode API keys, tokens, or connection strings in source code
|
||||
- Use environment variables, user secrets for local development, and a secret manager in production
|
||||
- Keep `appsettings.*.json` free of real credentials
|
||||
|
||||
```csharp
|
||||
// BAD
|
||||
const string ApiKey = "sk-live-123";
|
||||
|
||||
// GOOD
|
||||
var apiKey = builder.Configuration["OpenAI:ApiKey"]
|
||||
?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.");
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- Always use parameterized queries with ADO.NET, Dapper, or EF Core
|
||||
- Never concatenate user input into SQL strings
|
||||
- Validate sort fields and filter operators before using dynamic query composition
|
||||
|
||||
```csharp
|
||||
const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
|
||||
await connection.QueryAsync<Order>(sql, new { customerId });
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Validate DTOs at the application boundary
|
||||
- Use data annotations, FluentValidation, or explicit guard clauses
|
||||
- Reject invalid model state before running business logic
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
- Prefer framework auth handlers instead of custom token parsing
|
||||
- Enforce authorization policies at endpoint or handler boundaries
|
||||
- Never log raw tokens, passwords, or PII
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Return safe client-facing messages
|
||||
- Log detailed exceptions with structured context server-side
|
||||
- Do not expose stack traces, SQL text, or filesystem paths in API responses
|
||||
|
||||
## References
|
||||
|
||||
See skill: `security-review` for broader application security review checklists.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
---
|
||||
# C# Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with C#-specific content.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- Prefer **xUnit** for unit and integration tests
|
||||
- Use **FluentAssertions** for readable assertions
|
||||
- Use **Moq** or **NSubstitute** for mocking dependencies
|
||||
- Use **Testcontainers** when integration tests need real infrastructure
|
||||
|
||||
## Test Organization
|
||||
|
||||
- Mirror `src/` structure under `tests/`
|
||||
- Separate unit, integration, and end-to-end coverage clearly
|
||||
- Name tests by behavior, not implementation details
|
||||
|
||||
```csharp
|
||||
public sealed class OrderServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FindByIdAsync_ReturnsOrder_WhenOrderExists()
|
||||
{
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ASP.NET Core Integration Tests
|
||||
|
||||
- Use `WebApplicationFactory<TEntryPoint>` for API integration coverage
|
||||
- Test auth, validation, and serialization through HTTP, not by bypassing middleware
|
||||
|
||||
## Coverage
|
||||
|
||||
- Target 80%+ line coverage
|
||||
- Focus coverage on domain logic, validation, auth, and failure paths
|
||||
- Run `dotnet test` in CI with coverage collection enabled where available
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Go specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **gofmt** and **goimports** are mandatory — no style debates
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Accept interfaces, return structs
|
||||
- Keep interfaces small (1-3 methods)
|
||||
|
||||
## Error Handling
|
||||
|
||||
Always wrap errors with context:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `golang-patterns` for comprehensive Go idioms and patterns.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Go specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **gofmt/goimports**: Auto-format `.go` files after edit
|
||||
- **go vet**: Run static analysis after editing `.go` files
|
||||
- **staticcheck**: Run extended static checks on modified packages
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Go specific content.
|
||||
|
||||
## Functional Options
|
||||
|
||||
```go
|
||||
type Option func(*Server)
|
||||
|
||||
func WithPort(port int) Option {
|
||||
return func(s *Server) { s.port = port }
|
||||
}
|
||||
|
||||
func NewServer(opts ...Option) *Server {
|
||||
s := &Server{port: 8080}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
## Small Interfaces
|
||||
|
||||
Define interfaces where they are used, not where they are implemented.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Use constructor functions to inject dependencies:
|
||||
|
||||
```go
|
||||
func NewUserService(repo UserRepository, logger Logger) *UserService {
|
||||
return &UserService{repo: repo, logger: logger}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `golang-patterns` for comprehensive Go patterns including concurrency, error handling, and package organization.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Go specific content.
|
||||
|
||||
## Secret Management
|
||||
|
||||
```go
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("OPENAI_API_KEY not configured")
|
||||
}
|
||||
```
|
||||
|
||||
## Security Scanning
|
||||
|
||||
- Use **gosec** for static security analysis:
|
||||
```bash
|
||||
gosec ./...
|
||||
```
|
||||
|
||||
## Context & Timeouts
|
||||
|
||||
Always use `context.Context` for timeout control:
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Go specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use the standard `go test` with **table-driven tests**.
|
||||
|
||||
## Race Detection
|
||||
|
||||
Always run with the `-race` flag:
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `golang-testing` for detailed Go testing patterns and helpers.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Java-specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **google-java-format** or **Checkstyle** (Google or Sun style) for enforcement
|
||||
- One public top-level type per file
|
||||
- Consistent indent: 2 or 4 spaces (match project standard)
|
||||
- Member order: constants, fields, constructors, public methods, protected, private
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer `record` for value types (Java 16+)
|
||||
- Mark fields `final` by default — use mutable state only when required
|
||||
- Return defensive copies from public APIs: `List.copyOf()`, `Map.copyOf()`, `Set.copyOf()`
|
||||
- Copy-on-write: return new instances rather than mutating existing ones
|
||||
|
||||
```java
|
||||
// GOOD — immutable value type
|
||||
public record OrderSummary(Long id, String customerName, BigDecimal total) {}
|
||||
|
||||
// GOOD — final fields, no setters
|
||||
public class Order {
|
||||
private final Long id;
|
||||
private final List<LineItem> items;
|
||||
|
||||
public List<LineItem> getItems() {
|
||||
return List.copyOf(items);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
Follow standard Java conventions:
|
||||
- `PascalCase` for classes, interfaces, records, enums
|
||||
- `camelCase` for methods, fields, parameters, local variables
|
||||
- `SCREAMING_SNAKE_CASE` for `static final` constants
|
||||
- Packages: all lowercase, reverse domain (`com.example.app.service`)
|
||||
|
||||
## Modern Java Features
|
||||
|
||||
Use modern language features where they improve clarity:
|
||||
- **Records** for DTOs and value types (Java 16+)
|
||||
- **Sealed classes** for closed type hierarchies (Java 17+)
|
||||
- **Pattern matching** with `instanceof` — no explicit cast (Java 16+)
|
||||
- **Text blocks** for multi-line strings — SQL, JSON templates (Java 15+)
|
||||
- **Switch expressions** with arrow syntax (Java 14+)
|
||||
- **Pattern matching in switch** — exhaustive sealed type handling (Java 21+)
|
||||
|
||||
```java
|
||||
// Pattern matching instanceof
|
||||
if (shape instanceof Circle c) {
|
||||
return Math.PI * c.radius() * c.radius();
|
||||
}
|
||||
|
||||
// Sealed type hierarchy
|
||||
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
|
||||
|
||||
// Switch expression
|
||||
String label = switch (status) {
|
||||
case ACTIVE -> "Active";
|
||||
case SUSPENDED -> "Suspended";
|
||||
case CLOSED -> "Closed";
|
||||
};
|
||||
```
|
||||
|
||||
## Optional Usage
|
||||
|
||||
- Return `Optional<T>` from finder methods that may have no result
|
||||
- Use `map()`, `flatMap()`, `orElseThrow()` — never call `get()` without `isPresent()`
|
||||
- Never use `Optional` as a field type or method parameter
|
||||
|
||||
```java
|
||||
// GOOD
|
||||
return repository.findById(id)
|
||||
.map(ResponseDto::from)
|
||||
.orElseThrow(() -> new OrderNotFoundException(id));
|
||||
|
||||
// BAD — Optional as parameter
|
||||
public void process(Optional<String> name) {}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Prefer unchecked exceptions for domain errors
|
||||
- Create domain-specific exceptions extending `RuntimeException`
|
||||
- Avoid broad `catch (Exception e)` unless at top-level handlers
|
||||
- Include context in exception messages
|
||||
|
||||
```java
|
||||
public class OrderNotFoundException extends RuntimeException {
|
||||
public OrderNotFoundException(Long id) {
|
||||
super("Order not found: id=" + id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Streams
|
||||
|
||||
- Use streams for transformations; keep pipelines short (3-4 operations max)
|
||||
- Prefer method references when readable: `.map(Order::getTotal)`
|
||||
- Avoid side effects in stream operations
|
||||
- For complex logic, prefer a loop over a convoluted stream pipeline
|
||||
|
||||
## References
|
||||
|
||||
See skill: `java-coding-standards` for full coding standards with examples.
|
||||
See skill: `jpa-patterns` for JPA/Hibernate entity design patterns.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
- "**/pom.xml"
|
||||
- "**/build.gradle"
|
||||
- "**/build.gradle.kts"
|
||||
---
|
||||
# Java Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Java-specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **google-java-format**: Auto-format `.java` files after edit
|
||||
- **checkstyle**: Run style checks after editing Java files
|
||||
- **./mvnw compile** or **./gradlew compileJava**: Verify compilation after changes
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Java-specific content.
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
Encapsulate data access behind an interface:
|
||||
|
||||
```java
|
||||
public interface OrderRepository {
|
||||
Optional<Order> findById(Long id);
|
||||
List<Order> findAll();
|
||||
Order save(Order order);
|
||||
void deleteById(Long id);
|
||||
}
|
||||
```
|
||||
|
||||
Concrete implementations handle storage details (JPA, JDBC, in-memory for tests).
|
||||
|
||||
## Service Layer
|
||||
|
||||
Business logic in service classes; keep controllers and repositories thin:
|
||||
|
||||
```java
|
||||
public class OrderService {
|
||||
private final OrderRepository orderRepository;
|
||||
private final PaymentGateway paymentGateway;
|
||||
|
||||
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
|
||||
this.orderRepository = orderRepository;
|
||||
this.paymentGateway = paymentGateway;
|
||||
}
|
||||
|
||||
public OrderSummary placeOrder(CreateOrderRequest request) {
|
||||
var order = Order.from(request);
|
||||
paymentGateway.charge(order.total());
|
||||
var saved = orderRepository.save(order);
|
||||
return OrderSummary.from(saved);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constructor Injection
|
||||
|
||||
Always use constructor injection — never field injection:
|
||||
|
||||
```java
|
||||
// GOOD — constructor injection (testable, immutable)
|
||||
public class NotificationService {
|
||||
private final EmailSender emailSender;
|
||||
|
||||
public NotificationService(EmailSender emailSender) {
|
||||
this.emailSender = emailSender;
|
||||
}
|
||||
}
|
||||
|
||||
// BAD — field injection (untestable without reflection, requires framework magic)
|
||||
public class NotificationService {
|
||||
@Inject // or @Autowired
|
||||
private EmailSender emailSender;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO Mapping
|
||||
|
||||
Use records for DTOs. Map at service/controller boundaries:
|
||||
|
||||
```java
|
||||
public record OrderResponse(Long id, String customer, BigDecimal total) {
|
||||
public static OrderResponse from(Order order) {
|
||||
return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
Use for objects with many optional parameters:
|
||||
|
||||
```java
|
||||
public class SearchCriteria {
|
||||
private final String query;
|
||||
private final int page;
|
||||
private final int size;
|
||||
private final String sortBy;
|
||||
|
||||
private SearchCriteria(Builder builder) {
|
||||
this.query = builder.query;
|
||||
this.page = builder.page;
|
||||
this.size = builder.size;
|
||||
this.sortBy = builder.sortBy;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String query = "";
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sortBy = "id";
|
||||
|
||||
public Builder query(String query) { this.query = query; return this; }
|
||||
public Builder page(int page) { this.page = page; return this; }
|
||||
public Builder size(int size) { this.size = size; return this; }
|
||||
public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; }
|
||||
public SearchCriteria build() { return new SearchCriteria(this); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sealed Types for Domain Models
|
||||
|
||||
```java
|
||||
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {
|
||||
record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
|
||||
record PaymentFailure(String errorCode, String message) implements PaymentResult {}
|
||||
}
|
||||
|
||||
// Exhaustive handling (Java 21+)
|
||||
String message = switch (result) {
|
||||
case PaymentSuccess s -> "Paid: " + s.transactionId();
|
||||
case PaymentFailure f -> "Failed: " + f.errorCode();
|
||||
};
|
||||
```
|
||||
|
||||
## API Response Envelope
|
||||
|
||||
Consistent API responses:
|
||||
|
||||
```java
|
||||
public record ApiResponse<T>(boolean success, T data, String error) {
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(true, data, null);
|
||||
}
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(false, null, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `springboot-patterns` for Spring Boot architecture patterns.
|
||||
See skill: `jpa-patterns` for entity design and query optimization.
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Java-specific content.
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- Never hardcode API keys, tokens, or credentials in source code
|
||||
- Use environment variables: `System.getenv("API_KEY")`
|
||||
- Use a secret manager (Vault, AWS Secrets Manager) for production secrets
|
||||
- Keep local config files with secrets in `.gitignore`
|
||||
|
||||
```java
|
||||
// BAD
|
||||
private static final String API_KEY = "sk-abc123...";
|
||||
|
||||
// GOOD — environment variable
|
||||
String apiKey = System.getenv("PAYMENT_API_KEY");
|
||||
Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- Always use parameterized queries — never concatenate user input into SQL
|
||||
- Use `PreparedStatement` or your framework's parameterized query API
|
||||
- Validate and sanitize any input used in native queries
|
||||
|
||||
```java
|
||||
// BAD — SQL injection via string concatenation
|
||||
Statement stmt = conn.createStatement();
|
||||
String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
|
||||
stmt.executeQuery(sql);
|
||||
|
||||
// GOOD — PreparedStatement with parameterized query
|
||||
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
|
||||
ps.setString(1, name);
|
||||
|
||||
// GOOD — JDBC template
|
||||
jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Validate all user input at system boundaries before processing
|
||||
- Use Bean Validation (`@NotNull`, `@NotBlank`, `@Size`) on DTOs when using a validation framework
|
||||
- Sanitize file paths and user-provided strings before use
|
||||
- Reject input that fails validation with clear error messages
|
||||
|
||||
```java
|
||||
// Validate manually in plain Java
|
||||
public Order createOrder(String customerName, BigDecimal amount) {
|
||||
if (customerName == null || customerName.isBlank()) {
|
||||
throw new IllegalArgumentException("Customer name is required");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("Amount must be positive");
|
||||
}
|
||||
return new Order(customerName, amount);
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
- Never implement custom auth crypto — use established libraries
|
||||
- Store passwords with bcrypt or Argon2, never MD5/SHA1
|
||||
- Enforce authorization checks at service boundaries
|
||||
- Clear sensitive data from logs — never log passwords, tokens, or PII
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- Run `mvn dependency:tree` or `./gradlew dependencies` to audit transitive dependencies
|
||||
- Use OWASP Dependency-Check or Snyk to scan for known CVEs
|
||||
- Keep dependencies updated — set up Dependabot or Renovate
|
||||
|
||||
## Error Messages
|
||||
|
||||
- Never expose stack traces, internal paths, or SQL errors in API responses
|
||||
- Map exceptions to safe, generic client messages at handler boundaries
|
||||
- Log detailed errors server-side; return generic messages to clients
|
||||
|
||||
```java
|
||||
// Log the detail, return a generic message
|
||||
try {
|
||||
return orderService.findById(id);
|
||||
} catch (OrderNotFoundException ex) {
|
||||
log.warn("Order not found: id={}", id);
|
||||
return ApiResponse.error("Resource not found"); // generic, no internals
|
||||
} catch (Exception ex) {
|
||||
log.error("Unexpected error processing order id={}", id, ex);
|
||||
return ApiResponse.error("Internal server error"); // never expose ex.getMessage()
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `springboot-security` for Spring Security authentication and authorization patterns.
|
||||
See skill: `security-review` for general security checklists.
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Java-specific content.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`)
|
||||
- **AssertJ** for fluent assertions (`assertThat(result).isEqualTo(expected)`)
|
||||
- **Mockito** for mocking dependencies
|
||||
- **Testcontainers** for integration tests requiring databases or services
|
||||
|
||||
## Test Organization
|
||||
|
||||
```
|
||||
src/test/java/com/example/app/
|
||||
service/ # Unit tests for service layer
|
||||
controller/ # Web layer / API tests
|
||||
repository/ # Data access tests
|
||||
integration/ # Cross-layer integration tests
|
||||
```
|
||||
|
||||
Mirror the `src/main/java` package structure in `src/test/java`.
|
||||
|
||||
## Unit Test Pattern
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderServiceTest {
|
||||
|
||||
@Mock
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
private OrderService orderService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
orderService = new OrderService(orderRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("findById returns order when exists")
|
||||
void findById_existingOrder_returnsOrder() {
|
||||
var order = new Order(1L, "Alice", BigDecimal.TEN);
|
||||
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
|
||||
|
||||
var result = orderService.findById(1L);
|
||||
|
||||
assertThat(result.customerName()).isEqualTo("Alice");
|
||||
verify(orderRepository).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("findById throws when order not found")
|
||||
void findById_missingOrder_throws() {
|
||||
when(orderRepository.findById(99L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> orderService.findById(99L))
|
||||
.isInstanceOf(OrderNotFoundException.class)
|
||||
.hasMessageContaining("99");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parameterized Tests
|
||||
|
||||
```java
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"100.00, 10, 90.00",
|
||||
"50.00, 0, 50.00",
|
||||
"200.00, 25, 150.00"
|
||||
})
|
||||
@DisplayName("discount applied correctly")
|
||||
void applyDiscount(BigDecimal price, int pct, BigDecimal expected) {
|
||||
assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Use Testcontainers for real database integration:
|
||||
|
||||
```java
|
||||
@Testcontainers
|
||||
class OrderRepositoryIT {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
|
||||
|
||||
private OrderRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
var dataSource = new PGSimpleDataSource();
|
||||
dataSource.setUrl(postgres.getJdbcUrl());
|
||||
dataSource.setUser(postgres.getUsername());
|
||||
dataSource.setPassword(postgres.getPassword());
|
||||
repository = new JdbcOrderRepository(dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_and_findById() {
|
||||
var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
|
||||
var found = repository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Spring Boot integration tests, see skill: `springboot-tdd`.
|
||||
|
||||
## Test Naming
|
||||
|
||||
Use descriptive names with `@DisplayName`:
|
||||
- `methodName_scenario_expectedBehavior()` for method names
|
||||
- `@DisplayName("human-readable description")` for reports
|
||||
|
||||
## Coverage
|
||||
|
||||
- Target 80%+ line coverage
|
||||
- Use JaCoCo for coverage reporting
|
||||
- Focus on service and domain logic — skip trivial getters/config classes
|
||||
|
||||
## References
|
||||
|
||||
See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
|
||||
See skill: `java-coding-standards` for testing expectations.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Kotlin-specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **ktlint** or **Detekt** for style enforcement
|
||||
- Official Kotlin code style (`kotlin.code.style=official` in `gradle.properties`)
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer `val` over `var` — default to `val` and only use `var` when mutation is required
|
||||
- Use `data class` for value types; use immutable collections (`List`, `Map`, `Set`) in public APIs
|
||||
- Copy-on-write for state updates: `state.copy(field = newValue)`
|
||||
|
||||
## Naming
|
||||
|
||||
Follow Kotlin conventions:
|
||||
- `camelCase` for functions and properties
|
||||
- `PascalCase` for classes, interfaces, objects, and type aliases
|
||||
- `SCREAMING_SNAKE_CASE` for constants (`const val` or `@JvmStatic`)
|
||||
- Prefix interfaces with behavior, not `I`: `Clickable` not `IClickable`
|
||||
|
||||
## Null Safety
|
||||
|
||||
- Never use `!!` — prefer `?.`, `?:`, `requireNotNull()`, or `checkNotNull()`
|
||||
- Use `?.let {}` for scoped null-safe operations
|
||||
- Return nullable types from functions that can legitimately have no result
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val name = user!!.name
|
||||
|
||||
// GOOD
|
||||
val name = user?.name ?: "Unknown"
|
||||
val name = requireNotNull(user) { "User must be set before accessing name" }.name
|
||||
```
|
||||
|
||||
## Sealed Types
|
||||
|
||||
Use sealed classes/interfaces to model closed state hierarchies:
|
||||
|
||||
```kotlin
|
||||
sealed interface UiState<out T> {
|
||||
data object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(val message: String) : UiState<Nothing>
|
||||
}
|
||||
```
|
||||
|
||||
Always use exhaustive `when` with sealed types — no `else` branch.
|
||||
|
||||
## Extension Functions
|
||||
|
||||
Use extension functions for utility operations, but keep them discoverable:
|
||||
- Place in a file named after the receiver type (`StringExt.kt`, `FlowExt.kt`)
|
||||
- Keep scope limited — don't add extensions to `Any` or overly generic types
|
||||
|
||||
## Scope Functions
|
||||
|
||||
Use the right scope function:
|
||||
- `let` — null check + transform: `user?.let { greet(it) }`
|
||||
- `run` — compute a result using receiver: `service.run { fetch(config) }`
|
||||
- `apply` — configure an object: `builder.apply { timeout = 30 }`
|
||||
- `also` — side effects: `result.also { log(it) }`
|
||||
- Avoid deep nesting of scope functions (max 2 levels)
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Use `Result<T>` or custom sealed types
|
||||
- Use `runCatching {}` for wrapping throwable code
|
||||
- Never catch `CancellationException` — always rethrow it
|
||||
- Avoid `try-catch` for control flow
|
||||
|
||||
```kotlin
|
||||
// BAD — using exceptions for control flow
|
||||
val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
|
||||
|
||||
// GOOD — nullable return
|
||||
val user: User? = repository.findUser(id)
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
- "**/build.gradle.kts"
|
||||
---
|
||||
# Kotlin Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Kotlin-specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **ktfmt/ktlint**: Auto-format `.kt` and `.kts` files after edit
|
||||
- **detekt**: Run static analysis after editing Kotlin files
|
||||
- **./gradlew build**: Verify compilation after changes
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Kotlin and Android/KMP-specific content.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Prefer constructor injection. Use Koin (KMP) or Hilt (Android-only):
|
||||
|
||||
```kotlin
|
||||
// Koin — declare modules
|
||||
val dataModule = module {
|
||||
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
||||
factory { GetItemsUseCase(get()) }
|
||||
viewModelOf(::ItemListViewModel)
|
||||
}
|
||||
|
||||
// Hilt — annotations
|
||||
@HiltViewModel
|
||||
class ItemListViewModel @Inject constructor(
|
||||
private val getItems: GetItemsUseCase
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
## ViewModel Pattern
|
||||
|
||||
Single state object, event sink, one-way data flow:
|
||||
|
||||
```kotlin
|
||||
data class ScreenState(
|
||||
val items: List<Item> = emptyList(),
|
||||
val isLoading: Boolean = false
|
||||
)
|
||||
|
||||
class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ScreenState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: ScreenEvent) {
|
||||
when (event) {
|
||||
is ScreenEvent.Load -> load()
|
||||
is ScreenEvent.Delete -> delete(event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
- `suspend` functions return `Result<T>` or custom error type
|
||||
- `Flow` for reactive streams
|
||||
- Coordinate local + remote data sources
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getById(id: String): Result<Item>
|
||||
suspend fun getAll(): Result<List<Item>>
|
||||
fun observeAll(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## UseCase Pattern
|
||||
|
||||
Single responsibility, `operator fun invoke`:
|
||||
|
||||
```kotlin
|
||||
class GetItemUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(id: String): Result<Item> {
|
||||
return repository.getById(id)
|
||||
}
|
||||
}
|
||||
|
||||
class GetItemsUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(): Result<List<Item>> {
|
||||
return repository.getAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## expect/actual (KMP)
|
||||
|
||||
Use for platform-specific implementations:
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun platformName(): String
|
||||
expect class SecureStorage {
|
||||
fun save(key: String, value: String)
|
||||
fun get(key: String): String?
|
||||
}
|
||||
|
||||
// androidMain
|
||||
actual fun platformName(): String = "Android"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
|
||||
actual fun get(key: String): String? = null /* ... */
|
||||
}
|
||||
|
||||
// iosMain
|
||||
actual fun platformName(): String = "iOS"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* Keychain */ }
|
||||
actual fun get(key: String): String? = null /* ... */
|
||||
}
|
||||
```
|
||||
|
||||
## Coroutine Patterns
|
||||
|
||||
- Use `viewModelScope` in ViewModels, `coroutineScope` for structured child work
|
||||
- Use `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)` for StateFlow from cold Flows
|
||||
- Use `supervisorScope` when child failures should be independent
|
||||
|
||||
## Builder Pattern with DSL
|
||||
|
||||
```kotlin
|
||||
class HttpClientConfig {
|
||||
var baseUrl: String = ""
|
||||
var timeout: Long = 30_000
|
||||
private val interceptors = mutableListOf<Interceptor>()
|
||||
|
||||
fun interceptor(block: () -> Interceptor) {
|
||||
interceptors.add(block())
|
||||
}
|
||||
}
|
||||
|
||||
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
|
||||
val config = HttpClientConfig().apply(block)
|
||||
return HttpClient(config)
|
||||
}
|
||||
|
||||
// Usage
|
||||
val client = httpClient {
|
||||
baseUrl = "https://api.example.com"
|
||||
timeout = 15_000
|
||||
interceptor { AuthInterceptor(tokenProvider) }
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `kotlin-coroutines-flows` for detailed coroutine patterns.
|
||||
See skill: `android-clean-architecture` for module and layer patterns.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Kotlin and Android/KMP-specific content.
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- Never hardcode API keys, tokens, or credentials in source code
|
||||
- Use `local.properties` (git-ignored) for local development secrets
|
||||
- Use `BuildConfig` fields generated from CI secrets for release builds
|
||||
- Use `EncryptedSharedPreferences` (Android) or Keychain (iOS) for runtime secret storage
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val apiKey = "sk-abc123..."
|
||||
|
||||
// GOOD — from BuildConfig (generated at build time)
|
||||
val apiKey = BuildConfig.API_KEY
|
||||
|
||||
// GOOD — from secure storage at runtime
|
||||
val token = secureStorage.get("auth_token")
|
||||
```
|
||||
|
||||
## Network Security
|
||||
|
||||
- Use HTTPS exclusively — configure `network_security_config.xml` to block cleartext
|
||||
- Pin certificates for sensitive endpoints using OkHttp `CertificatePinner` or Ktor equivalent
|
||||
- Set timeouts on all HTTP clients — never leave defaults (which may be infinite)
|
||||
- Validate and sanitize all server responses before use
|
||||
|
||||
```xml
|
||||
<!-- res/xml/network_security_config.xml -->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Validate all user input before processing or sending to API
|
||||
- Use parameterized queries for Room/SQLDelight — never concatenate user input into SQL
|
||||
- Sanitize file paths from user input to prevent path traversal
|
||||
|
||||
```kotlin
|
||||
// BAD — SQL injection
|
||||
@Query("SELECT * FROM items WHERE name = '$input'")
|
||||
|
||||
// GOOD — parameterized
|
||||
@Query("SELECT * FROM items WHERE name = :input")
|
||||
fun findByName(input: String): List<ItemEntity>
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
- Use `EncryptedSharedPreferences` for sensitive key-value data on Android
|
||||
- Use `@Serializable` with explicit field names — don't leak internal property names
|
||||
- Clear sensitive data from memory when no longer needed
|
||||
- Use `@Keep` or ProGuard rules for serialized classes to prevent name mangling
|
||||
|
||||
## Authentication
|
||||
|
||||
- Store tokens in secure storage, not in plain SharedPreferences
|
||||
- Implement token refresh with proper 401/403 handling
|
||||
- Clear all auth state on logout (tokens, cached user data, cookies)
|
||||
- Use biometric authentication (`BiometricPrompt`) for sensitive operations
|
||||
|
||||
## ProGuard / R8
|
||||
|
||||
- Keep rules for all serialized models (`@Serializable`, Gson, Moshi)
|
||||
- Keep rules for reflection-based libraries (Koin, Retrofit)
|
||||
- Test release builds — obfuscation can break serialization silently
|
||||
|
||||
## WebView Security
|
||||
|
||||
- Disable JavaScript unless explicitly needed: `settings.javaScriptEnabled = false`
|
||||
- Validate URLs before loading in WebView
|
||||
- Never expose `@JavascriptInterface` methods that access sensitive data
|
||||
- Use `WebViewClient.shouldOverrideUrlLoading()` to control navigation
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Kotlin and Android/KMP-specific content.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- **kotlin.test** for multiplatform (KMP) — `@Test`, `assertEquals`, `assertTrue`
|
||||
- **JUnit 4/5** for Android-specific tests
|
||||
- **Turbine** for testing Flows and StateFlow
|
||||
- **kotlinx-coroutines-test** for coroutine testing (`runTest`, `TestDispatcher`)
|
||||
|
||||
## ViewModel Testing with Turbine
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `loading state emitted then data`() = runTest {
|
||||
val repo = FakeItemRepository()
|
||||
repo.addItem(testItem)
|
||||
val viewModel = ItemListViewModel(GetItemsUseCase(repo))
|
||||
|
||||
viewModel.state.test {
|
||||
assertEquals(ItemListState(), awaitItem()) // initial state
|
||||
viewModel.onEvent(ItemListEvent.Load)
|
||||
assertTrue(awaitItem().isLoading) // loading
|
||||
assertEquals(listOf(testItem), awaitItem().items) // loaded
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fakes Over Mocks
|
||||
|
||||
Prefer hand-written fakes over mocking frameworks:
|
||||
|
||||
```kotlin
|
||||
class FakeItemRepository : ItemRepository {
|
||||
private val items = mutableListOf<Item>()
|
||||
var fetchError: Throwable? = null
|
||||
|
||||
override suspend fun getAll(): Result<List<Item>> {
|
||||
fetchError?.let { return Result.failure(it) }
|
||||
return Result.success(items.toList())
|
||||
}
|
||||
|
||||
override fun observeAll(): Flow<List<Item>> = flowOf(items.toList())
|
||||
|
||||
fun addItem(item: Item) { items.add(item) }
|
||||
}
|
||||
```
|
||||
|
||||
## Coroutine Testing
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `parallel operations complete`() = runTest {
|
||||
val repo = FakeRepository()
|
||||
val result = loadDashboard(repo)
|
||||
advanceUntilIdle()
|
||||
assertNotNull(result.items)
|
||||
assertNotNull(result.stats)
|
||||
}
|
||||
```
|
||||
|
||||
Use `runTest` — it auto-advances virtual time and provides `TestScope`.
|
||||
|
||||
## Ktor MockEngine
|
||||
|
||||
```kotlin
|
||||
val mockEngine = MockEngine { request ->
|
||||
when (request.url.encodedPath) {
|
||||
"/api/items" -> respond(
|
||||
content = Json.encodeToString(testItems),
|
||||
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
)
|
||||
else -> respondError(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
val client = HttpClient(mockEngine) {
|
||||
install(ContentNegotiation) { json() }
|
||||
}
|
||||
```
|
||||
|
||||
## Room/SQLDelight Testing
|
||||
|
||||
- Room: Use `Room.inMemoryDatabaseBuilder()` for in-memory testing
|
||||
- SQLDelight: Use `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` for JVM tests
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `insert and query items`() = runTest {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
Database.Schema.create(driver)
|
||||
val db = Database(driver)
|
||||
|
||||
db.itemQueries.insert("1", "Sample Item", "description")
|
||||
val items = db.itemQueries.getAll().executeAsList()
|
||||
assertEquals(1, items.size)
|
||||
}
|
||||
```
|
||||
|
||||
## Test Naming
|
||||
|
||||
Use backtick-quoted descriptive names:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `search with empty query returns all items`() = runTest { }
|
||||
|
||||
@Test
|
||||
fun `delete item emits updated list without deleted item`() = runTest { }
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository)
|
||||
├── androidUnitTest/kotlin/ # Android unit tests (JUnit)
|
||||
├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI)
|
||||
└── iosTest/kotlin/ # iOS-specific tests
|
||||
```
|
||||
|
||||
Minimum test coverage: ViewModel + UseCase for every feature.
|
||||
@@ -0,0 +1,616 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
---
|
||||
# Lua Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Lua specific content.
|
||||
|
||||
## Version Compatibility Matrix
|
||||
|
||||
These rules target **universal Lua coverage** across 5.0, 5.1, 5.2, 5.3, and 5.4. When a feature differs between versions, use the portable pattern or guard with a version check.
|
||||
|
||||
| Feature | 5.0 | 5.1 | 5.2 | 5.3 | 5.4 | Portable Pattern |
|
||||
|---------|-----|-----|-----|-----|-----|------------------|
|
||||
| Length operator `#` | ❌ `table.getn` | ✅ | ✅ | ✅ | ✅ | `table.getn(t)` or `#t` with guard |
|
||||
| Varargs `...` as expr | ❌ `arg` table | ✅ | ✅ | ✅ | ✅ | See Varargs section |
|
||||
| `select()` | ❌ | ✅ | ✅ | ✅ | ✅ | Guard: `if select then ... else arg[i]` |
|
||||
| `math.mod` / `math.fmod` | ✅ `math.mod` | ✅ `math.fmod` | ✅ `math.fmod` | ✅ | ✅ | `math.fmod or math.mod` |
|
||||
| `string.gmatch` | ❌ `string.gfind` | ✅ | ✅ | ✅ | ✅ | `string.gmatch or string.gfind` |
|
||||
| `setfenv`/`getfenv` | ✅ | ✅ | ❌ `_ENV` | ❌ | ❌ | Version-gated sandbox |
|
||||
| `unpack()` global | ✅ | ✅ | ❌ `table.unpack` | ❌ | ❌ | `unpack or table.unpack` |
|
||||
| `xpcall` extra args | ❌ | ❌ | ✅ | ✅ | ✅ | Wrap in closure for 5.0/5.1 |
|
||||
| `goto` statement | ❌ | ❌ | ✅ | ✅ | ✅ | Avoid; use early return |
|
||||
| Integer subtype | ❌ | ❌ | ❌ | ✅ | ✅ | All numbers are doubles in 5.0–5.2 |
|
||||
| Bitwise operators | ❌ | ❌ | ✅ `bit32` lib | ✅ native (`bit32` deprecated) | ✅ native | `bit32` lib on 5.2; native ops on 5.3+ |
|
||||
| `__gc` for tables | ❌ | ❌ | ✅ | ✅ | ✅ | Only for userdata in 5.0/5.1 |
|
||||
| `__len` for tables | ❌ | ❌ (userdata only) | ✅ | ✅ | ✅ | `rawlen` or custom function; 5.1 `__len` only for userdata |
|
||||
| `table.move` | ❌ | ❌ | ❌ | ✅ | ✅ | Manual loop |
|
||||
| `table.foreach`/`foreachi` | ✅ | ✅ (deprecated) | ❌ removed | ❌ | ❌ | Use `pairs`/`ipairs` (5.0+) |
|
||||
| `table.setn` | ✅ | ✅ (deprecated) | ❌ removed | ❌ | ❌ | Track length manually |
|
||||
| `table.maxn` | ❌ | ✅ | ✅ (deprecated) | ❌ removed | ❌ | Manual loop over keys |
|
||||
| `package` table | ❌ `loadlib` | ✅ | ✅ | ✅ | ✅ | Guard `package and package.path` |
|
||||
| `pcall` extra args | ✅ | ✅ | ✅ | ✅ | ✅ | Universal |
|
||||
| `coroutine.status` | ✅ | ✅ | ✅ | ✅ | ✅ | Universal |
|
||||
| `table.pack` | ❌ | ❌ | ✅ | ✅ | ✅ | `{n = select("#", ...), ...}` |
|
||||
| `rawlen()` | ❌ | ❌ | ✅ | ✅ | ✅ | `rawlen or function(t) return #t end` |
|
||||
| `loadstring` | ✅ | ✅ | ❌ (deprecated→`load`) | ❌ | ❌ | `loadstring or load` |
|
||||
| `string.pack/unpack` | ❌ | ❌ | ❌ | ✅ | ✅ | External `struct` lib for older versions |
|
||||
| `utf8` library | ❌ | ❌ | ❌ | ✅ | ✅ | External `lua-utf8` lib for older |
|
||||
| `package.loaders` | ❌ | ✅ | ❌→`.searchers` | ❌ | ❌ | `package.loaders or package.searchers` |
|
||||
| `coroutine.isyieldable` | ❌ | ❌ | ❌ | ✅ | ✅ | Guard: `coroutine.isyieldable and ...` |
|
||||
| `math.atan2` | ✅ | ✅ | ✅ | ✅ | ✅ | `math.atan(y, x)` — note argument order is (y, x), not (x, y) |
|
||||
| `math.log10` | ❌ | ✅ | ✅ | ❌ removed | ❌ | `math.log(x, 10)` or `math.log(x) / math.log(10)` |
|
||||
| `math.pow` | ✅ | ✅ | ✅ | ✅ | ✅ | `x ^ y` (native operator) or `math.pow(x, y)` |
|
||||
| `math.log(x, base)` | ❌ | ❌ | ✅ | ✅ | ✅ | `math.log(x) / math.log(base)` for portable base |
|
||||
| Floor division `//` | ❌ | ❌ | ❌ | ✅ | ✅ | `math.floor(a / b)` for 5.0–5.2 |
|
||||
| `math.cosh/sinh/tanh` | ❌ | ✅ | ✅ | ❌ deprecated | ❌ | Implement manually or use external lib |
|
||||
| `math.frexp`/`math.ldexp` | ❌ | ✅ | ✅ | ❌ deprecated | ❌ | `x * 2.0^exp` for ldexp; external lib for frexp |
|
||||
| `coroutine.close` | ❌ | ❌ | ❌ | ❌ | ✅ | Guard: `coroutine.close and coroutine.close(co)` |
|
||||
| `warn()` function | ❌ | ❌ | ❌ | ❌ | ✅ | Guard: `if warn then warn(msg) end` |
|
||||
| `<const>`/`<close>` attrs | ❌ | ❌ | ❌ | ❌ | ✅ | Use `local` without attrs on 5.0–5.3 |
|
||||
| `__le` metamethod required | ❌ (derived from `__lt`) | ❌ | ❌ | ❌ | ✅ (must define explicitly) | Always define both `__lt` and `__le` |
|
||||
| String→number coercion | ✅ auto | ✅ auto | ✅ auto | ✅ auto | ❌ removed from core | Use explicit `tonumber()` for portability |
|
||||
| Long string nesting `[[]]` | ✅ (nestable) | ❌ (no nesting) | ❌ | ❌ | ❌ | Use `[=[...]=]` for nested long strings |
|
||||
| `%z` pattern class | ✅ | ✅ | ❌ deprecated | ❌ | ❌ | Use literal `\0` to match the null character (ASCII 0) in patterns (5.2+) |
|
||||
| `__ipairs` metamethod | ❌ | ❌ | ✅ | ❌ deprecated | ❌ | Avoid; `ipairs` uses raw integer keys |
|
||||
| Float→string `.0` suffix | ❌ `2.0`→`"2"` | ❌ | ❌ | ✅ `2.0`→`"2.0"` | ✅ | Use `string.format` for consistent formatting |
|
||||
| `print` calls `tostring` | ✅ | ✅ | ✅ | ✅ | ❌ (hardwired) | Use `__tostring` metamethod for custom output |
|
||||
| `math.random` auto-seeded | ❌ | ❌ | ❌ | ❌ | ✅ (new PRNG) | Call `math.randomseed` explicitly on 5.0–5.3 |
|
||||
| `math.log(x, base)` | ❌ | ❌ | ✅ | ✅ | ✅ | `math.log(x) / math.log(base)` for base argument |
|
||||
| Floor division `//` | ❌ | ❌ | ❌ | ✅ | ✅ | `math.floor(a / b)` for 5.0–5.2 |
|
||||
| `io.lines` return count | 1 | 1 | 1 | 1 | 4 (line, extra, line number, error) | Wrap in `(io.lines(...))` to get 1 value |
|
||||
| `collectgarbage("count")` | N/A | 2 values | 2 values | 1 value | 1 value | Use `math.floor(collectgarbage("count"))` |
|
||||
|
||||
### Cross-Version Compatibility Shim
|
||||
|
||||
Place at the top of your entry point file to normalize APIs across versions:
|
||||
|
||||
```lua
|
||||
-- compat.lua: Universal Lua 5.0–5.4 shim
|
||||
-- Place at top of your entry point; all other files use these locals.
|
||||
|
||||
-- Core builtins that moved between versions
|
||||
local unpack = unpack or table.unpack -- 5.2 moved to table
|
||||
local getn = table.getn or function(t) return #t end -- 5.0 has no #
|
||||
local setn = table.setn or function() end -- 5.0 tracks length via setn
|
||||
local maxn = table.maxn or function(t) -- removed in 5.3
|
||||
local n = 0
|
||||
for k in pairs(t) do
|
||||
if type(k) == "number" and k > n then n = k end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
-- String library renames
|
||||
local gmatch = string.gmatch or string.gfind -- 5.0 uses gfind
|
||||
|
||||
-- Math library renames
|
||||
local fmod = math.fmod or math.mod -- 5.0 uses math.mod
|
||||
|
||||
-- Module system: 5.0 has no package table
|
||||
local loadlib = package and package.loadlib or loadlib
|
||||
|
||||
-- Vararg helpers
|
||||
local getVarargCount = select -- nil in 5.0
|
||||
and function(...) return select("#", ...) end -- 5.1+: use select
|
||||
or function() return arg and arg.n or 0 end -- 5.0: use arg.n
|
||||
|
||||
-- Code loading: loadstring renamed to load in 5.2
|
||||
local loadstring = loadstring or load -- 5.2+ removed loadstring
|
||||
|
||||
-- Raw operations: rawlen added in 5.2 (bypasses __len)
|
||||
local rawlen = rawlen or function(v) return #v end -- fallback uses #
|
||||
|
||||
-- table.pack: added in 5.2; polyfill for 5.0/5.1
|
||||
local table_pack = table.pack or function(...)
|
||||
return { n = select and select("#", ...) or (arg and arg.n or 0), ... }
|
||||
end
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
- **StyLua** for code formatting — always run `stylua .` before committing
|
||||
- **Luacheck** for lints — `luacheck . --no-color` (treat warnings as CI failures)
|
||||
- 4-space indent (StyLua default)
|
||||
- Max line width: 120 characters
|
||||
- Trailing commas in multi-line table constructors (prevents noisy diffs)
|
||||
|
||||
## Naming
|
||||
|
||||
Follow standard Lua conventions:
|
||||
- `camelCase` for local variables, functions, method names
|
||||
- `PascalCase` for modules, "classes" (tables used as classes via metatables)
|
||||
- `UPPER_SNAKE_CASE` for constants and enum-like values
|
||||
- `_camelCase` (leading underscore) for private/internal helpers
|
||||
- `UPPER_SNAKE_CASE` for WoW event strings (`"PLAYER_LOGIN"`, `"QUEST_ACCEPTED"`)
|
||||
|
||||
```lua
|
||||
-- GOOD
|
||||
local QuestieDB = {} -- PascalCase module
|
||||
local MAX_RETRIES = 3 -- UPPER_SNAKE constant
|
||||
local questId = 10141 -- camelCase local
|
||||
local function _countTable(t) -- _camelCase private helper
|
||||
```
|
||||
|
||||
## Scoping: local Is Non-Negotiable
|
||||
|
||||
Every variable MUST be `local` unless explicitly required as a global. Globals leak into `_G`, pollute the namespace, introduce untraceable coupling between files, and cause **ADDON_ACTION_BLOCKED** taint in WoW sandboxes.
|
||||
|
||||
```lua
|
||||
-- BAD: Implicit global — leaks into _G, causes taint
|
||||
myValue = 10
|
||||
|
||||
-- GOOD: Explicit local
|
||||
local myValue = 10
|
||||
```
|
||||
|
||||
**Global caching**: Cache frequently-called global functions into module-level locals. This provides measurable performance improvement in tight loops (Lua resolves locals via stack slot, globals via hash lookup into `_G`).
|
||||
|
||||
```lua
|
||||
-- Cache at file / module top — BEFORE any function definitions
|
||||
local type = type
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local next = next
|
||||
local tostring = tostring
|
||||
local tinsert = table.insert
|
||||
local tremove = table.remove
|
||||
local tconcat = table.concat
|
||||
local format = string.format
|
||||
local floor = math.floor
|
||||
local max = math.max
|
||||
local min = math.min
|
||||
local coroutine = coroutine
|
||||
local setmetatable = setmetatable
|
||||
local getmetatable = getmetatable
|
||||
```
|
||||
|
||||
## Immutability
|
||||
|
||||
> **Language note**: This rule overrides [common/coding-style.md](../common/coding-style.md)'s strict immutability. Lua tables are mutable by design and creating fresh copies on every update is prohibitively expensive for large data sets (e.g., 40,000+ NPC records). Use immutability where practical; use controlled mutation with clear ownership semantics where performance demands it.
|
||||
|
||||
When immutability is practical (configuration, options, small payloads):
|
||||
|
||||
```lua
|
||||
-- GOOD — returns a new table with the field updated
|
||||
local function withField(original, key, value)
|
||||
local copy = {}
|
||||
for k, v in pairs(original) do copy[k] = v end
|
||||
copy[key] = value
|
||||
return copy
|
||||
end
|
||||
```
|
||||
|
||||
When mutation is necessary (hot paths, large tables, WoW frame pools), document ownership:
|
||||
|
||||
```lua
|
||||
-- ACCEPTABLE — mutation of owned table, clearly documented
|
||||
--- Compiles NPC data into the binary cache. Mutates QuestieDB.npcData in place
|
||||
--- because copying 40k records per compile is prohibitively expensive.
|
||||
function QuestieDBCompiler:CompileNPCData(sourceData, targetTable)
|
||||
for id, data in pairs(sourceData) do
|
||||
targetTable[id] = self:EncodeRow(data)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Use **read-only proxies** for tables that must not be modified after initialization:
|
||||
|
||||
```lua
|
||||
local function readOnly(t)
|
||||
return setmetatable({}, {
|
||||
__index = t,
|
||||
__newindex = function(_, k, _)
|
||||
error(format("Attempt to modify read-only table at key: %s", tostring(k)), 2)
|
||||
end,
|
||||
-- table.getn for 5.0 compat; # for 5.1+
|
||||
__len = function() return (table.getn or rawlen or function(x) return #x end)(t) end,
|
||||
})
|
||||
end
|
||||
|
||||
local QUEST_FLAGS = readOnly({ SHARABLE = 0x0008, DAILY = 0x1000 })
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
### Construction
|
||||
|
||||
Prefer table literals over sequential assignment. The compiler generates fewer instructions and programmer intent is clearer:
|
||||
|
||||
```lua
|
||||
-- GOOD: Single allocation, clear structure
|
||||
local config = {
|
||||
maxRetries = 3,
|
||||
timeout = 5,
|
||||
version = "1.4.7",
|
||||
pluginNames = { "WotLKDB", "TurtleDB" },
|
||||
}
|
||||
|
||||
-- BAD: Multiple allocations, fragmented intent
|
||||
local config = {}
|
||||
config.maxRetries = 3
|
||||
config.timeout = 5
|
||||
config.version = "1.4.7"
|
||||
config.pluginNames = {}
|
||||
config.pluginNames[1] = "WotLKDB"
|
||||
config.pluginNames[2] = "TurtleDB"
|
||||
```
|
||||
|
||||
### Array Building in Loops
|
||||
|
||||
Use `t[#t + 1]` (fastest in Lua 5.1+) or `tinsert`. In Lua 5.0, use `tinsert` or track an index manually since `#` does not exist:
|
||||
|
||||
```lua
|
||||
-- GOOD: Fastest array append (Lua 5.1+)
|
||||
local results = {}
|
||||
for id, data in pairs(sourceData) do
|
||||
results[#results + 1] = data
|
||||
end
|
||||
|
||||
-- GOOD: Works on ALL versions (5.0–5.4)
|
||||
local results = {}
|
||||
for id, data in pairs(sourceData) do
|
||||
tinsert(results, data)
|
||||
end
|
||||
|
||||
-- GOOD: Manual index tracking (universal, fastest in 5.0)
|
||||
local results = {}
|
||||
local n = 0
|
||||
for id, data in pairs(sourceData) do
|
||||
n = n + 1
|
||||
results[n] = data
|
||||
end
|
||||
```
|
||||
|
||||
### Iteration Patterns
|
||||
|
||||
| Use case | Pattern | Lua versions | Notes |
|
||||
|----------|---------|-------------|-------|
|
||||
| Sequential array (no holes) | `for i = 1, #t do` | 5.1+ | Fastest; no function call overhead |
|
||||
| Sequential array (universal) | `for i = 1, table.getn(t) do` | 5.0+ | Use `getn` shim for 5.0 compat |
|
||||
| Array with value | `for i, v in ipairs(t) do` | 5.0+ | Stops at first `nil` hole |
|
||||
| Dictionary / sparse table | `for k, v in pairs(t) do` | 5.0+ | Unordered; processes every key |
|
||||
| Universal / taint-free | `for k, v in next, t do` | 5.0+ | Equivalent to `pairs` but avoids metamethod |
|
||||
| Empty-check | `if next(t) == nil then` | 5.0+ | Only reliable way |
|
||||
| Counting elements | Custom `_countTable` function | 5.0+ | `#t`/`table.getn` only counts array part |
|
||||
|
||||
```lua
|
||||
-- Empty-check: ONLY correct way
|
||||
if next(myTable) == nil then
|
||||
-- Table is truly empty (no array or hash keys)
|
||||
end
|
||||
|
||||
-- WRONG: Unreliable for dictionaries and sparse arrays
|
||||
if #myTable == 0 then -- BROKEN on { a = 1, b = 2 }
|
||||
```
|
||||
|
||||
### Holes in Arrays
|
||||
|
||||
The `#` operator is **undefined** on sparse arrays (arrays with `nil` gaps). If holes are possible, track length with a counter field or use a dedicated array class.
|
||||
|
||||
```lua
|
||||
-- WRONG: Undefined behavior with holes
|
||||
local t = { 1, nil, 3 }
|
||||
print(#t) -- Could be 1 or 3 (implementation-dependent)
|
||||
|
||||
-- CORRECT: Track length explicitly
|
||||
local t = { n = 3; 1, nil, 3 }
|
||||
for i = 1, t.n do print(t[i]) end
|
||||
```
|
||||
|
||||
### Table Recycling and Wipe
|
||||
|
||||
Reuse tables to reduce GC pressure in hot paths. Use `wipe()` (WoW) or manual niling:
|
||||
|
||||
```lua
|
||||
-- WoW environment: wipe() clears all keys
|
||||
wipe(myTable)
|
||||
|
||||
-- Standard Lua: manual clear
|
||||
for k in pairs(myTable) do myTable[k] = nil end
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
- Keep functions under 50 lines. Extract helpers prefixed with `_`.
|
||||
- **Return early** to flatten nesting. Lua has no guard clauses, so explicit early returns are the idiomatic substitute.
|
||||
- Avoid deep nesting (> 4 levels). Refactor into helper functions.
|
||||
|
||||
```lua
|
||||
-- BAD: Deep nesting
|
||||
function processQuest(questId)
|
||||
if questId then
|
||||
local data = QuestieDB.questData[questId]
|
||||
if data then
|
||||
local name = data[1]
|
||||
if name then
|
||||
-- 4 levels deep...
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- GOOD: Early returns flatten the code
|
||||
function processQuest(questId)
|
||||
if not questId then return end
|
||||
local data = QuestieDB.questData[questId]
|
||||
if not data then return end
|
||||
local name = data[1]
|
||||
if not name then return end
|
||||
-- Proceed at 1 level of nesting
|
||||
end
|
||||
```
|
||||
|
||||
### Default Arguments
|
||||
|
||||
Lua has no native defaults. Use the `or` idiom for simple cases, explicit `nil` checks for values where `false` or `0` are valid:
|
||||
|
||||
```lua
|
||||
-- Simple default (WRONG if false/0 are valid values)
|
||||
local function greet(name, greeting)
|
||||
name = name or "Adventurer"
|
||||
greeting = greeting or "Hello"
|
||||
return format("%s, %s!", greeting, name)
|
||||
end
|
||||
|
||||
-- Precise default (handles false/0 correctly)
|
||||
local function setEnabled(flag)
|
||||
if flag == nil then flag = true end -- Default to true; false is a valid input
|
||||
end
|
||||
```
|
||||
|
||||
### Varargs
|
||||
|
||||
Varargs behavior differs significantly across Lua versions:
|
||||
|
||||
| Version | Access pattern | Length |
|
||||
|---------|---------------|--------|
|
||||
| 5.0 | `arg` table (auto-created) | `arg.n` |
|
||||
| 5.1+ | `...` as expression | `select("#", ...)` |
|
||||
|
||||
```lua
|
||||
-- Lua 5.1+: Use select for length (handles trailing nil)
|
||||
local function logAll(...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n do
|
||||
local v = select(i, ...)
|
||||
print(format("arg[%d] = %s", i, tostring(v)))
|
||||
end
|
||||
end
|
||||
|
||||
-- Lua 5.0: Use the implicit 'arg' table
|
||||
local function logAll(...) -- 5.0 creates 'arg' automatically
|
||||
for i = 1, arg.n do
|
||||
print(format("arg[%d] = %s", i, tostring(arg[i])))
|
||||
end
|
||||
end
|
||||
|
||||
-- UNIVERSAL: Works on 5.0–5.4
|
||||
local function logAll(...)
|
||||
local args = select and { n = select("#", ...), ... } or arg
|
||||
for i = 1, args.n do
|
||||
print(format("arg[%d] = %s", i, tostring(args[i])))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
- **Short concatenation**: `..` is fine for 2–3 pieces.
|
||||
- **Loop-built strings**: Use `table.concat` to avoid O(n²) intermediate string allocations.
|
||||
- **Structured output**: Prefer `string.format` over `..` chains for readability and localization.
|
||||
|
||||
```lua
|
||||
-- BAD: O(n²) string growth in a loop
|
||||
local result = ""
|
||||
for _, v in ipairs(data) do
|
||||
result = result .. tostring(v) .. ", "
|
||||
end
|
||||
|
||||
-- GOOD: O(n) via table.concat
|
||||
local parts = {}
|
||||
for i, v in ipairs(data) do
|
||||
parts[i] = tostring(v)
|
||||
end
|
||||
local result = tconcat(parts, ", ")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Lua uses `pcall` / `xpcall` as its try/catch equivalent. Use them at system boundaries (event handlers, plugin entry points, data loading). NEVER silently swallow errors.
|
||||
|
||||
```lua
|
||||
-- pcall: Returns ok, result_or_error
|
||||
local ok, err = pcall(function()
|
||||
dangerousOperation()
|
||||
end)
|
||||
if not ok then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL,
|
||||
"[Module] Operation failed: " .. tostring(err))
|
||||
end
|
||||
|
||||
-- xpcall: Adds a message handler for stack traces
|
||||
local function errorHandler(msg)
|
||||
return tostring(msg) .. "\n" .. debugstack(2)
|
||||
end
|
||||
|
||||
local ok, result = xpcall(dangerousOperation, errorHandler)
|
||||
```
|
||||
|
||||
**Version note**: In Lua 5.0 and 5.1, `xpcall` does NOT support passing arguments to the called function. Wrap in a closure:
|
||||
|
||||
```lua
|
||||
-- WRONG in 5.0/5.1: xpcall does not forward args
|
||||
local ok, result = xpcall(dangerousOp, errorHandler, arg1, arg2)
|
||||
|
||||
-- CORRECT (universal): Wrap in closure
|
||||
local ok, result = xpcall(function()
|
||||
return dangerousOp(arg1, arg2)
|
||||
end, errorHandler)
|
||||
```
|
||||
|
||||
### Error Propagation
|
||||
|
||||
Lua uses the `success, result` multi-return pattern (no exceptions). Propagate errors by returning `nil, errorMessage`:
|
||||
|
||||
```lua
|
||||
local function loadConfig(path)
|
||||
local f, err = io.open(path, "r")
|
||||
if not f then return nil, "Cannot open: " .. tostring(err) end
|
||||
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
|
||||
if not content or content == "" then
|
||||
return nil, "Empty config file: " .. path
|
||||
end
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
-- Caller
|
||||
local config, err = loadConfig("settings.ini")
|
||||
if not config then
|
||||
error("Config load failed: " .. err)
|
||||
end
|
||||
```
|
||||
|
||||
## Nil Guards and Defensive Access
|
||||
|
||||
Always check `nil` before indexing. Chain guards for deep access:
|
||||
|
||||
```lua
|
||||
-- Safe deep access (short-circuit on nil)
|
||||
local name = myData and myData.info and myData.info.name
|
||||
|
||||
-- Explicit nil check when 0, false, or "" are valid values
|
||||
if myData.count ~= nil then
|
||||
processCount(myData.count) -- count could be 0
|
||||
end
|
||||
```
|
||||
|
||||
## Metatables
|
||||
|
||||
Use metatables for `__index` delegation, operator overloads, `__tostring`, and read-only enforcement. **Never** expose raw metatables of security-sensitive tables publicly; use `__metatable` to guard them.
|
||||
|
||||
```lua
|
||||
-- Guard metatable from external manipulation
|
||||
local Secret = {}
|
||||
Secret.__index = Secret
|
||||
Secret.__metatable = "Access denied" -- getmetatable() returns this string
|
||||
|
||||
function Secret:New(value)
|
||||
return setmetatable({ _value = value }, Secret)
|
||||
end
|
||||
```
|
||||
|
||||
### Common Metamethods
|
||||
|
||||
| Metamethod | Purpose | Example |
|
||||
|-----------|---------|---------|
|
||||
| `__index` | Delegation / inheritance | Class systems, proxy tables |
|
||||
| `__newindex` | Intercept writes | Read-only guards, validation |
|
||||
| `__tostring` | Custom `tostring()` output | Debug printing |
|
||||
| `__call` | Make a table callable | Functor pattern |
|
||||
| `__len` | Custom `#` operator | Tables: 5.2+ only; userdata: 5.1+; ignored for tables in 5.0/5.1 |
|
||||
| `__eq`, `__lt`, `__le` | Comparison operators | 5.0+; **5.4**: `__le` must be explicit (no longer derived from `__lt`) |
|
||||
| `__gc` | Garbage collection finalizer | Tables: 5.2+ only; userdata: 5.0+ |
|
||||
| `__close` | To-be-closed variable cleanup | 5.4+ only; `local x <close> = resource` |
|
||||
| `__metatable` | Protect from `getmetatable` | Security-sensitive objects (5.0+) |
|
||||
| `__concat` | Custom `..` operator | String-like objects (5.0+) |
|
||||
|
||||
## Coroutines
|
||||
|
||||
Use coroutines for cooperative multitasking — chunked database compilation, lazy iteration over large datasets, and frame-budgeted work in game engines.
|
||||
|
||||
**Critical Lua 5.0/5.1 limitation**: You cannot `yield` from inside a `pcall`/`xpcall` call. This throws `"cannot resume dead coroutine"`. Structure your code so the yield happens outside the protected call. This was fixed in Lua 5.2+ (yield-across-pcall support).
|
||||
|
||||
**Lua 5.0 note**: The full `coroutine` library (`create`, `resume`, `yield`, `status`, `wrap`) is available in Lua 5.0. However, in 5.0, you cannot yield from inside a C function, a metamethod, or an iterator — only from the main coroutine body. This restriction was relaxed in 5.1 (yield from iterators became possible) and further in 5.2 (yield across `pcall`/`xpcall`).
|
||||
|
||||
```lua
|
||||
local fmod = math.fmod or math.mod -- 5.0 has math.mod, 5.1+ has math.fmod
|
||||
|
||||
local co = coroutine.create(function()
|
||||
for i = 1, 10000 do
|
||||
processRecord(i)
|
||||
if fmod(i, 100) == 0 then -- 5.0: no % operator; use fmod shim
|
||||
coroutine.yield() -- Pause every 100 records
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Resume from a frame ticker (WoW: C_Timer.After, OnUpdate)
|
||||
local function tick()
|
||||
if coroutine.status(co) ~= "dead" then
|
||||
local ok, err = coroutine.resume(co)
|
||||
if not ok then
|
||||
error("Coroutine error: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Numeric Precision
|
||||
|
||||
Lua 5.0–5.2 use 64-bit IEEE doubles for **all** numbers (no integer subtype). Lua 5.3+ introduced a separate integer subtype. Integers above 2^53 lose precision in double-only versions. Be aware of floating-point edge cases:
|
||||
|
||||
```lua
|
||||
-- WRONG: Float comparison
|
||||
if result == 0.3 then -- May fail due to IEEE 754
|
||||
|
||||
-- CORRECT: Epsilon comparison
|
||||
local EPSILON = 1e-9
|
||||
if math.abs(result - 0.3) < EPSILON then
|
||||
```
|
||||
|
||||
## Debug Output
|
||||
|
||||
- **No `print()` in production code** — it bypasses logging levels, cannot be filtered, and causes taint in WoW.
|
||||
- Gate debug output behind a severity level flag.
|
||||
- Use structured debug helpers.
|
||||
|
||||
```lua
|
||||
-- BAD
|
||||
print("Loading quest " .. questId)
|
||||
|
||||
-- GOOD
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP,
|
||||
format("[QuestieDB] Loading quest %d", questId))
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
MANY SMALL FILES > FEW LARGE FILES:
|
||||
- 200–400 lines typical, 800 lines absolute maximum
|
||||
- One module / class per file
|
||||
- Group by feature/domain, not by type
|
||||
- Data files (large lookup tables) may exceed 800 lines — exclude them from line-count rules
|
||||
|
||||
```text
|
||||
Database/
|
||||
├── QuestieDB.lua # Core query interface
|
||||
├── compiler.lua # Binary compilation
|
||||
├── Corrections/
|
||||
│ ├── QuestCorrections.lua
|
||||
│ └── NPCCorrections.lua
|
||||
├── Data/
|
||||
│ ├── questData.lua # Raw data (exempt from 800-line rule)
|
||||
│ └── npcData.lua
|
||||
└── Zones/
|
||||
└── zoneDB.lua
|
||||
```
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
Before marking work complete on any Lua file:
|
||||
- [ ] All variables are `local` (zero undeclared globals)
|
||||
- [ ] Functions are under 50 lines
|
||||
- [ ] Files are under 800 lines (data files exempt)
|
||||
- [ ] No nesting deeper than 4 levels
|
||||
- [ ] Error paths return `nil, errMsg` or log explicitly — never silently swallowed
|
||||
- [ ] No hardcoded magic numbers (use named constants)
|
||||
- [ ] Table iteration uses the correct primitive (`ipairs`, `pairs`, `next`, numeric `for`)
|
||||
- [ ] String building in loops uses `table.concat`, not `..`
|
||||
- [ ] `luacheck` passes with zero warnings
|
||||
- [ ] `stylua --check` passes
|
||||
@@ -0,0 +1,348 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
---
|
||||
# Lua Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Lua specific content.
|
||||
|
||||
## Tool Chain Summary
|
||||
|
||||
| Tool | Purpose | Install | Run |
|
||||
|------|---------|---------|-----|
|
||||
| **Luacheck** | Static analysis (undefined globals, unused vars, shadowed locals) | `luarocks install luacheck` | `luacheck .` |
|
||||
| **StyLua** | Opinionated code formatter (Rust-based, fast) | `cargo install stylua` | `stylua .` |
|
||||
| **Luacov** | Line coverage reporting | `luarocks install luacov` | `busted --coverage && luacov` |
|
||||
| **lua-language-server** | IDE diagnostics, type checking, completion | VS Code extension | Automatic |
|
||||
|
||||
## Static Analysis: Luacheck
|
||||
|
||||
Luacheck detects:
|
||||
- Undefined global variables (critical for preventing taint)
|
||||
- Unused local variables and function arguments
|
||||
- Shadowed local variables
|
||||
- Unreachable code after `return`
|
||||
- Unused values assigned to variables
|
||||
|
||||
### Configuration (`.luacheckrc`)
|
||||
|
||||
Place at project root. Be exhaustive with known globals to eliminate false positives.
|
||||
|
||||
**Version targeting**: Set `std` based on your runtime. Use `"lua50"` for 5.0, `"lua51"` for 5.1, or `"none"` for maximum strictness (recommended for WoW addons where you must declare every global):
|
||||
|
||||
```lua
|
||||
-- .luacheckrc
|
||||
-- For Lua 5.0 projects: use "lua50" to allow table.getn, table.setn, etc.
|
||||
-- For Lua 5.1 projects: use "lua51"
|
||||
-- For WoW addons: use "none" (strictest; manually declare all globals)
|
||||
std = "none"
|
||||
max_line_length = 120
|
||||
cache = true -- Speed up repeated runs
|
||||
|
||||
-- Allowed globals — explicitly list every WoW API function used
|
||||
globals = {
|
||||
-- Core addon system
|
||||
"QuestieLoader", "Questie", "QuestieDB",
|
||||
}
|
||||
|
||||
read_globals = {
|
||||
-- Lua builtins (read-only)
|
||||
"select", "unpack", "pcall", "xpcall", "error", "assert",
|
||||
"type", "tostring", "tonumber", "rawget", "rawset",
|
||||
"setmetatable", "getmetatable", "next", "pairs", "ipairs",
|
||||
"coroutine", "string", "table", "math", "bit",
|
||||
|
||||
-- WoW Frame API
|
||||
"CreateFrame", "UIParent",
|
||||
|
||||
-- WoW Timer API
|
||||
"C_Timer",
|
||||
|
||||
-- WoW Map API
|
||||
"C_Map", "C_QuestLog",
|
||||
|
||||
-- WoW Unit API
|
||||
"UnitGUID", "UnitName", "UnitLevel", "UnitFactionGroup",
|
||||
"UnitClass", "UnitRace", "GetRealmName",
|
||||
|
||||
-- WoW Addon API
|
||||
"IsAddOnLoaded", "GetAddOnInfo", "GetNumAddOns",
|
||||
"GetAddOnMetadata",
|
||||
|
||||
-- WoW Combat API
|
||||
"InCombatLockdown",
|
||||
|
||||
-- WoW Misc
|
||||
"Enum", "GetTime", "GetLocale", "GetBuildInfo",
|
||||
"SlashCmdList", "SLASH_QUESTIE1",
|
||||
"hooksecurefunc", "debugstack", "geterrorhandler",
|
||||
"print", "format", "wipe", "strsplit", "strtrim",
|
||||
"tinsert", "tremove",
|
||||
|
||||
-- Lua 5.0-specific globals (add if targeting 5.0)
|
||||
-- "loadlib", -- 5.0 global; moved to package.loadlib in 5.1+
|
||||
-- Note: table.getn, table.setn, table.foreach, table.foreachi,
|
||||
-- math.mod, and string.gfind are methods on their parent tables.
|
||||
-- Luacheck already allows them via the "table", "math", and "string"
|
||||
-- entries above. Use std = "lua50" if you need full 5.0 stdlib.
|
||||
|
||||
-- SavedVariables (read-only access is acceptable)
|
||||
"QuestieSV",
|
||||
}
|
||||
|
||||
-- Per-directory overrides
|
||||
files["Database/Data/**"] = {
|
||||
max_line_length = false, -- Data files have long lines
|
||||
ignore = { "631" }, -- Allow line length variance
|
||||
}
|
||||
|
||||
files["tests/**"] = {
|
||||
std = "+busted", -- Add Busted globals (describe, it, assert, etc.)
|
||||
globals = {
|
||||
"_G", -- Tests may manipulate _G for mocking
|
||||
},
|
||||
}
|
||||
|
||||
files["Localization/**"] = {
|
||||
max_line_length = false, -- Translation strings can be long
|
||||
}
|
||||
|
||||
-- Warnings to suppress project-wide
|
||||
ignore = {
|
||||
"212", -- Unused argument (common in callbacks: function(self, event, ...))
|
||||
"213", -- Unused loop variable (for _ in pairs)
|
||||
}
|
||||
```
|
||||
|
||||
### Running Luacheck
|
||||
|
||||
```bash
|
||||
# Full project lint
|
||||
luacheck .
|
||||
|
||||
# Single file with column info
|
||||
luacheck Database/QuestieDB.lua --codes --ranges
|
||||
|
||||
# CI mode: no color, non-zero exit on warnings
|
||||
luacheck . --no-color --formatter plain
|
||||
|
||||
# Show only errors (ignore warnings)
|
||||
luacheck . --only 0
|
||||
|
||||
# List all globals used (audit for taint)
|
||||
luacheck . --globals --no-unused --no-redefined
|
||||
```
|
||||
|
||||
### Common Warning Codes
|
||||
|
||||
| Code | Meaning | Fix |
|
||||
|------|---------|-----|
|
||||
| 111 | Setting undefined global | Add `local` or add to `globals` list |
|
||||
| 112 | Mutating undefined global | Same as 111 |
|
||||
| 113 | Accessing undefined global | Add to `read_globals` or add `local` |
|
||||
| 211 | Unused local variable | Remove or prefix with `_` |
|
||||
| 212 | Unused argument | Prefix with `_` or add to `ignore` |
|
||||
| 311 | Unused value | Remove the assignment |
|
||||
| 411 | Redefining local variable | Rename or restructure |
|
||||
| 421 | Shadowing local variable | Rename inner variable |
|
||||
| 542 | Empty if branch | Add logic or use guard pattern |
|
||||
|
||||
## Formatting: StyLua
|
||||
|
||||
### Configuration (`stylua.toml`)
|
||||
|
||||
```toml
|
||||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
collapse_simple_statement = "Never"
|
||||
|
||||
[sort_requires]
|
||||
enabled = false # Lua module loading order matters
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Check formatting without modifying (CI)
|
||||
stylua --check .
|
||||
|
||||
# Auto-format all Lua files
|
||||
stylua .
|
||||
|
||||
# Format a single file
|
||||
stylua Database/QuestieDB.lua
|
||||
|
||||
# Preview changes (diff mode)
|
||||
stylua --check --output-format=diff .
|
||||
```
|
||||
|
||||
## Language Server: lua-language-server (Sumneko)
|
||||
|
||||
### VS Code Configuration
|
||||
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
// Set to "Lua 5.0" for 5.0 projects, "Lua 5.1" for WoW, etc.
|
||||
"Lua.runtime.version": "Lua 5.1",
|
||||
"Lua.diagnostics.globals": [
|
||||
"Questie", "QuestieLoader", "QuestieDB",
|
||||
"CreateFrame", "C_Timer", "C_Map", "Enum",
|
||||
"GetTime", "IsAddOnLoaded", "InCombatLockdown",
|
||||
"hooksecurefunc", "debugstack", "wipe",
|
||||
"print", "format", "strsplit"
|
||||
],
|
||||
"Lua.workspace.library": [
|
||||
// Path to WoW API type definitions if available
|
||||
],
|
||||
"Lua.workspace.ignoreDir": [
|
||||
"Database/Data",
|
||||
".release"
|
||||
],
|
||||
"Lua.diagnostics.disable": [
|
||||
"lowercase-global"
|
||||
],
|
||||
"Lua.completion.callSnippet": "Replace",
|
||||
"Lua.hint.enable": true
|
||||
}
|
||||
```
|
||||
|
||||
### Type Annotations (EmmyLua / lua-language-server)
|
||||
|
||||
Use `---@` annotations to add type safety in supported IDEs:
|
||||
|
||||
```lua
|
||||
---@class QuestieDB
|
||||
---@field npcData table<number, table>
|
||||
---@field questData table<number, table>
|
||||
local QuestieDB = {}
|
||||
|
||||
---@param npcId number
|
||||
---@return table|nil npcData
|
||||
---@return string|nil errorMessage
|
||||
function QuestieDB:GetNPC(npcId)
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
|
||||
## Pre-Commit Hook
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# .git/hooks/pre-commit
|
||||
set -e
|
||||
|
||||
echo "=== Luacheck ==="
|
||||
luacheck . --no-color
|
||||
|
||||
echo "=== StyLua ==="
|
||||
stylua --check .
|
||||
|
||||
echo "=== All checks passed ==="
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
```makefile
|
||||
.PHONY: lint format test coverage ci
|
||||
|
||||
lint:
|
||||
luacheck . --no-color
|
||||
stylua --check .
|
||||
|
||||
format:
|
||||
stylua .
|
||||
|
||||
test:
|
||||
busted --verbose
|
||||
|
||||
coverage:
|
||||
busted --coverage
|
||||
luacov
|
||||
@awk '/^Total/ { if ($$4+0 < 80) { print "FAIL: Coverage " $$4 "% < 80%"; exit 1 } else { print "PASS: Coverage " $$4 "%"; } }' luacov.report.out
|
||||
|
||||
ci: lint test coverage
|
||||
```
|
||||
|
||||
## CI Pipeline (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/lua-ci.yml
|
||||
name: Lua CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Lua
|
||||
uses: leafo/gh-actions-lua@v10
|
||||
with:
|
||||
luaVersion: "5.1"
|
||||
|
||||
- name: Setup LuaRocks
|
||||
uses: leafo/gh-actions-luarocks@v4
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
luarocks install luacheck
|
||||
luarocks install busted
|
||||
luarocks install luacov
|
||||
|
||||
- name: Install StyLua
|
||||
run: |
|
||||
curl -L -o stylua.zip https://github.com/JohnnyMorganz/StyLua/releases/latest/download/stylua-linux-x86_64.zip
|
||||
unzip stylua.zip -d /usr/local/bin/
|
||||
chmod +x /usr/local/bin/stylua
|
||||
|
||||
- name: Lint (Luacheck)
|
||||
run: luacheck . --no-color
|
||||
|
||||
- name: Format Check (StyLua)
|
||||
run: stylua --check .
|
||||
|
||||
- name: Test
|
||||
run: busted --output=TAP --coverage
|
||||
|
||||
- name: Coverage
|
||||
run: |
|
||||
luacov
|
||||
awk '/^Total/ { if ($4+0 < 80) { print "FAIL: " $4 "%"; exit 1 } }' luacov.report.out
|
||||
```
|
||||
|
||||
## PostToolUse Hook Behavior
|
||||
|
||||
After every Lua file edit or creation, the agent SHOULD:
|
||||
|
||||
1. Run `luacheck <file>` on the modified file
|
||||
2. Run `stylua --check <file>` on the modified file
|
||||
3. Report any issues **before** proceeding to the next edit
|
||||
|
||||
This catches errors immediately rather than accumulating them across a multi-file change.
|
||||
|
||||
## WoW-Specific: Taint Detection
|
||||
|
||||
Monitor for these errors in the WoW error log — they indicate your addon is touching protected state:
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `ADDON_ACTION_BLOCKED` | Tainted code called a protected API | Remove the taint source (`loadstring`, `_G` writes) |
|
||||
| `ADDON_ACTION_FORBIDDEN` | Addon tried to call a hardware event API | Guard with `InCombatLockdown()` |
|
||||
| `Couldn't find frame` | Invalid secure template reference | Check template names in `CreateFrame` |
|
||||
|
||||
Common taint sources and their fixes:
|
||||
|
||||
| Taint Source | Fix |
|
||||
|-------------|-----|
|
||||
| `loadstring()` in addon code | Replace with function dispatch tables |
|
||||
| `_G.MyAddon_Data = data` | Use `addonTable` from the TOC vararg |
|
||||
| Writing globals from `OnUpdate` | Move writes to `ADDON_LOADED` or `PLAYER_LOGIN` |
|
||||
| `rawset(_G, name, value)` | Use local module tables instead |
|
||||
| Hooking with function replacement | Use `hooksecurefunc()` (post-hook only, never overwrite) |
|
||||
| Calling restricted APIs after taint | Isolate tainted code from secure code paths |
|
||||
@@ -0,0 +1,484 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
---
|
||||
# Lua Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Lua specific content.
|
||||
|
||||
## Module Pattern
|
||||
|
||||
The standard Lua module returns a table as its public API. All internal state and helpers are file-local. This is the foundation of all Lua architecture.
|
||||
|
||||
```lua
|
||||
-- mymodule.lua
|
||||
local M = {}
|
||||
|
||||
-- Private state — invisible outside this file
|
||||
local _cache = {}
|
||||
local _initialized = false
|
||||
|
||||
-- Private helper — underscore prefix signals internal use
|
||||
local function _buildCacheKey(id)
|
||||
return "key_" .. tostring(id)
|
||||
end
|
||||
|
||||
-- Public API — the only things callers can access
|
||||
function M.get(id)
|
||||
return _cache[_buildCacheKey(id)]
|
||||
end
|
||||
|
||||
function M.init()
|
||||
if _initialized then return end
|
||||
_initialized = true
|
||||
-- one-time setup
|
||||
end
|
||||
|
||||
return M
|
||||
```
|
||||
|
||||
### Legacy Module Styles (5.0 and 5.1)
|
||||
|
||||
In Lua 5.0, `setfenv` was used directly to create module environments (there was no `module()` function). In Lua 5.1, the `module()` built-in was introduced. Both are deprecated/removed in 5.2+:
|
||||
|
||||
```lua
|
||||
-- LEGACY 5.0 style — setfenv only (no module() function exists in 5.0)
|
||||
local M = {}
|
||||
setfenv(1, M) -- Sets the current function's environment to M
|
||||
|
||||
function get(id) -- Automatically scoped to M
|
||||
return cache[id]
|
||||
end
|
||||
return M
|
||||
|
||||
-- LEGACY 5.1 style — module() function (introduced in 5.1, deprecated in 5.2)
|
||||
module("mymodule") -- Creates a global table, sets the function env
|
||||
|
||||
function get(id) -- Automatically added to the module table
|
||||
return cache[id]
|
||||
end
|
||||
|
||||
-- MODERN style (5.0+) — return-table pattern works on ALL versions
|
||||
local M = {}
|
||||
function M.get(id) return cache[id] end
|
||||
return M
|
||||
```
|
||||
|
||||
**Rule**: Always use the return-table pattern. It works on Lua 5.0–5.4, does not depend on `setfenv` or `module()`, and does not pollute the global namespace.
|
||||
|
||||
## Loader / ImportModule Pattern
|
||||
|
||||
In large addon systems, a central loader avoids circular `require()` chains by acting as a module registry with lazy resolution:
|
||||
|
||||
```lua
|
||||
-- QuestieLoader.lua
|
||||
local Loader = {}
|
||||
local _modules = {}
|
||||
|
||||
function Loader:CreateModule(name)
|
||||
local mod = {}
|
||||
_modules[name] = mod
|
||||
return mod
|
||||
end
|
||||
|
||||
function Loader:ImportModule(name)
|
||||
return _modules[name]
|
||||
end
|
||||
|
||||
-- Usage in a module file:
|
||||
local QuestieDB = QuestieLoader:CreateModule("QuestieDB")
|
||||
|
||||
-- Usage as a consumer:
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
```
|
||||
|
||||
This pattern breaks circular dependencies because modules register themselves at parse time but only call `ImportModule` at runtime (inside function bodies).
|
||||
|
||||
## OOP / "Class" Pattern
|
||||
|
||||
Simulate classes and inheritance with metatables. This is the most common OOP approach in Lua.
|
||||
|
||||
### Basic Class
|
||||
|
||||
```lua
|
||||
local Animal = {}
|
||||
Animal.__index = Animal
|
||||
|
||||
function Animal:New(name, sound)
|
||||
return setmetatable({
|
||||
name = name,
|
||||
sound = sound,
|
||||
}, self) -- 'self' is Animal here (or a subclass)
|
||||
end
|
||||
|
||||
function Animal:Speak()
|
||||
return self.name .. " says " .. self.sound
|
||||
end
|
||||
```
|
||||
|
||||
### Inheritance
|
||||
|
||||
Chain `__index` through the class hierarchy. Call parent constructors with dot-notation (not colon) to avoid double-wrapping `self`:
|
||||
|
||||
```lua
|
||||
-- Dog extends Animal
|
||||
local Dog = setmetatable({}, { __index = Animal })
|
||||
Dog.__index = Dog
|
||||
|
||||
function Dog:New(name)
|
||||
-- CORRECT: dot-notation passes 'self' (Dog) explicitly
|
||||
local instance = Animal.New(self, name, "Woof")
|
||||
return setmetatable(instance, Dog)
|
||||
end
|
||||
|
||||
function Dog:Fetch(item)
|
||||
return self.name .. " fetches the " .. item
|
||||
end
|
||||
|
||||
-- BAD: colon-notation would pass Dog as 'self' twice
|
||||
-- local instance = Animal:New(name, "Woof") -- WRONG
|
||||
```
|
||||
|
||||
### Mixins
|
||||
|
||||
Add behavior from multiple sources without full multiple inheritance:
|
||||
|
||||
```lua
|
||||
local Serializable = {}
|
||||
function Serializable:Serialize()
|
||||
local parts = {}
|
||||
for k, v in pairs(self) do
|
||||
table.insert(parts, tostring(k) .. "=" .. tostring(v))
|
||||
end
|
||||
return table.concat(parts, ";")
|
||||
end
|
||||
|
||||
-- Mixin application
|
||||
local function applyMixin(class, mixin)
|
||||
for k, v in pairs(mixin) do
|
||||
if class[k] == nil then -- Don't overwrite existing methods
|
||||
class[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
applyMixin(Dog, Serializable)
|
||||
-- Now Dog instances have :Serialize()
|
||||
```
|
||||
|
||||
## Singleton Pattern
|
||||
|
||||
Wrap initialization in a one-time guard. Common for managers, registries, and services:
|
||||
|
||||
```lua
|
||||
local ConfigManager = {}
|
||||
local _config = nil
|
||||
|
||||
function ConfigManager:Get(key)
|
||||
if not _config then
|
||||
_config = self:_load()
|
||||
end
|
||||
return _config[key]
|
||||
end
|
||||
|
||||
function ConfigManager:_load()
|
||||
-- expensive one-time load
|
||||
return { debug = false, maxLevel = 80 }
|
||||
end
|
||||
```
|
||||
|
||||
## Registry / Plugin Architecture
|
||||
|
||||
Use a central registry for runtime plugin discovery without hard coupling. This is the pattern used by `QuestiePluginAPI`:
|
||||
|
||||
```lua
|
||||
local PluginRegistry = { _plugins = {} }
|
||||
|
||||
function PluginRegistry:Register(name, pluginData)
|
||||
assert(type(name) == "string", "Plugin name must be a string")
|
||||
assert(not self._plugins[name], "Plugin already registered: " .. name)
|
||||
|
||||
local plugin = {
|
||||
name = name,
|
||||
data = pluginData or {},
|
||||
stats = { QUEST = 0, NPC = 0, OBJECT = 0, ITEM = 0 },
|
||||
}
|
||||
self._plugins[name] = plugin
|
||||
return plugin
|
||||
end
|
||||
|
||||
function PluginRegistry:Get(name)
|
||||
return self._plugins[name]
|
||||
end
|
||||
|
||||
function PluginRegistry:IsAnyLoaded()
|
||||
return next(self._plugins) ~= nil
|
||||
end
|
||||
```
|
||||
|
||||
### Plugin-Side Registration
|
||||
|
||||
Plugins register at parse time (top of file, outside any event handler) so the core can discover them immediately:
|
||||
|
||||
```lua
|
||||
-- In plugin Loader.lua (top level, not inside PLAYER_LOGIN)
|
||||
local plugin = PluginRegistry:Register("WotLKDB", addonTable)
|
||||
```
|
||||
|
||||
## Observer / Event Bus Pattern
|
||||
|
||||
Decouple producers from consumers. Essential for addon systems where modules load in unpredictable order:
|
||||
|
||||
```lua
|
||||
local EventBus = { _listeners = {} }
|
||||
|
||||
function EventBus:On(event, fn)
|
||||
self._listeners[event] = self._listeners[event] or {}
|
||||
-- Use table.insert for 5.0 compat (no # operator)
|
||||
table.insert(self._listeners[event], fn)
|
||||
end
|
||||
|
||||
function EventBus:Off(event, fn)
|
||||
local listeners = self._listeners[event]
|
||||
if not listeners then return end
|
||||
-- Reverse iterate; table.getn for 5.0, # for 5.1+
|
||||
local n = table.getn and table.getn(listeners) or #listeners
|
||||
for i = n, 1, -1 do
|
||||
if listeners[i] == fn then
|
||||
table.remove(listeners, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function EventBus:Emit(event, ...)
|
||||
local listeners = self._listeners[event]
|
||||
if not listeners then return end
|
||||
-- ipairs works on all versions (5.0+)
|
||||
for _, fn in ipairs(listeners) do
|
||||
fn(...) -- 5.1+: ... is an expression; 5.0: use unpack(arg) instead
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Coroutine-Based Lazy Iterator
|
||||
|
||||
Turn expensive database scans into resumable, lazy iterations without building the full result set in memory:
|
||||
|
||||
```lua
|
||||
local function filteredNPCs(npcData, predicate)
|
||||
return coroutine.wrap(function()
|
||||
for id, data in pairs(npcData) do
|
||||
if data and predicate(id, data) then
|
||||
coroutine.yield(id, data)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Usage: processes NPCs one at a time, no intermediate table
|
||||
for npcId, npcData in filteredNPCs(QuestieDB.npcData, function(id, d)
|
||||
return d[2] and d[2] >= 70 -- level >= 70
|
||||
end) do
|
||||
processNPC(npcId, npcData)
|
||||
end
|
||||
```
|
||||
|
||||
**Version note**: `coroutine.wrap` is available in Lua 5.0+. However, in 5.0 you cannot yield from inside an iterator used in a generic `for` loop as part of a C boundary. The wrap-based pattern above works because the `for` loop drives `resume` directly.
|
||||
|
||||
## Chunked Processing (Frame-Budgeted Work)
|
||||
|
||||
For operations that must not freeze the game client, split work across frames:
|
||||
|
||||
```lua
|
||||
local function createChunkedProcessor(items, processFunc, chunkSize)
|
||||
chunkSize = chunkSize or 100
|
||||
local keys = {}
|
||||
-- Use table.insert for 5.0 compat
|
||||
for k in pairs(items) do table.insert(keys, k) end
|
||||
|
||||
local index = 1
|
||||
local total = table.getn and table.getn(keys) or #keys
|
||||
|
||||
return function() -- Call this each frame/tick
|
||||
local budget = math.min(index + chunkSize - 1, total)
|
||||
for i = index, budget do
|
||||
processFunc(keys[i], items[keys[i]])
|
||||
end
|
||||
index = budget + 1
|
||||
return index > total -- Returns true when done
|
||||
end
|
||||
end
|
||||
|
||||
-- Usage with WoW C_Timer
|
||||
local processor = createChunkedProcessor(rawData, compileRecord, 200)
|
||||
local ticker
|
||||
ticker = C_Timer.NewTicker(0.01, function()
|
||||
if processor() then
|
||||
ticker:Cancel()
|
||||
print("Compilation complete!")
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
## Memoization / Caching
|
||||
|
||||
Cache expensive computations. Support cache invalidation:
|
||||
|
||||
```lua
|
||||
local _zoneCache = {}
|
||||
local _zoneCacheDirty = false
|
||||
|
||||
function ZoneDB:GetZoneAreaId(uiMapId)
|
||||
if not _zoneCacheDirty and _zoneCache[uiMapId] ~= nil then
|
||||
return _zoneCache[uiMapId]
|
||||
end
|
||||
local result = self:_computeZoneAreaId(uiMapId)
|
||||
_zoneCache[uiMapId] = result
|
||||
return result
|
||||
end
|
||||
|
||||
function ZoneDB:InvalidateCache()
|
||||
wipe(_zoneCache)
|
||||
_zoneCacheDirty = false
|
||||
end
|
||||
```
|
||||
|
||||
**Caution**: Use `~= nil` for the cache check, not truthiness. Cached `false` or `0` values are valid and must not trigger recomputation.
|
||||
|
||||
## AddonTable Pattern (WoW-Specific)
|
||||
|
||||
The WoW client passes a private shared table to every file listed in the addon's `.toc`:
|
||||
|
||||
```lua
|
||||
-- File 1: data.lua
|
||||
local addonName, addonTable = ...
|
||||
addonTable.npcData = { [1] = { "Ragnaros", 63, 1 } }
|
||||
|
||||
-- File 2: init.lua — same addonTable reference
|
||||
local addonName, addonTable = ...
|
||||
local npc = addonTable.npcData[1]
|
||||
print(npc[1]) -- "Ragnaros"
|
||||
```
|
||||
|
||||
**Critical rule**: Use `addonTable` as the **sole** inter-file communication channel. Never use `_G` for data sharing between files — it pollutes the namespace, causes taint, and is visible to every addon in the client.
|
||||
|
||||
**Lua 5.0 note**: The vararg `...` syntax to capture `addonName, addonTable` works differently in 5.0. In 5.0, you must use the implicit `arg` table:
|
||||
|
||||
```lua
|
||||
-- Lua 5.1+:
|
||||
local addonName, addonTable = ...
|
||||
|
||||
-- Lua 5.0:
|
||||
local addonName = arg[1]
|
||||
local addonTable = arg[2]
|
||||
|
||||
-- Universal (works on 5.0–5.4):
|
||||
local addonName = select and select(1, ...) or (arg and arg[1])
|
||||
local addonTable = select and select(2, ...) or (arg and arg[2])
|
||||
```
|
||||
|
||||
## Proxy / Facade Pattern
|
||||
|
||||
Wrap a complex subsystem behind a simplified interface:
|
||||
|
||||
```lua
|
||||
local QuestieAPI = {}
|
||||
|
||||
function QuestieAPI:GetQuestName(questId)
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
local data = QuestieDB and QuestieDB:GetQuest(questId)
|
||||
return data and data.name or "Unknown Quest"
|
||||
end
|
||||
```
|
||||
|
||||
## Functional Patterns
|
||||
|
||||
Lua supports higher-order functions. Use them for data transformations:
|
||||
|
||||
```lua
|
||||
-- map: Transform each element (uses ipairs for 5.0 compat)
|
||||
local function map(t, fn)
|
||||
local result = {}
|
||||
for i, v in ipairs(t) do result[i] = fn(v, i) end
|
||||
return result
|
||||
end
|
||||
|
||||
-- filter: Keep elements matching predicate
|
||||
local function filter(t, predicate)
|
||||
local result = {}
|
||||
for i, v in ipairs(t) do
|
||||
if predicate(v, i) then table.insert(result, v) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- reduce: Fold elements into a single value
|
||||
local function reduce(t, fn, initial)
|
||||
local acc = initial
|
||||
for i, v in ipairs(t) do acc = fn(acc, v, i) end
|
||||
return acc
|
||||
end
|
||||
|
||||
-- Compose: Right-to-left function composition
|
||||
-- Note: Uses arg.n in 5.0 since select() doesn't exist
|
||||
local function compose(...)
|
||||
local fns = select and { ... } or arg
|
||||
local n = select and select("#", ...) or fns.n
|
||||
return function(x)
|
||||
for i = n, 1, -1 do x = fns[i](x) end
|
||||
return x
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Enumerations
|
||||
|
||||
Lua has no native enums. Simulate with constant tables. Optionally freeze with `readOnly()` (see coding-style.md):
|
||||
|
||||
```lua
|
||||
local DebugLevel = {
|
||||
CRITICAL = 1,
|
||||
INFO = 2,
|
||||
DEVELOP = 3,
|
||||
}
|
||||
|
||||
local QuestFlags = {
|
||||
SHARABLE = 0x0008,
|
||||
DAILY = 0x1000,
|
||||
WEEKLY = 0x8000,
|
||||
AUTO_ACCEPT = 0x80000,
|
||||
}
|
||||
```
|
||||
|
||||
## Weak Tables
|
||||
|
||||
Use weak references for caches that should not prevent garbage collection:
|
||||
|
||||
```lua
|
||||
-- Values are weak: GC can collect them when no other references exist
|
||||
local textureCache = setmetatable({}, { __mode = "v" })
|
||||
|
||||
function getTexture(path)
|
||||
local cached = textureCache[path]
|
||||
if cached then return cached end
|
||||
local tex = loadTexture(path)
|
||||
textureCache[path] = tex
|
||||
return tex
|
||||
end
|
||||
```
|
||||
|
||||
| Mode | Meaning |
|
||||
|------|---------|
|
||||
| `__mode = "v"` | Weak values — GC collects values with no other refs |
|
||||
| `__mode = "k"` | Weak keys — GC collects keys with no other refs |
|
||||
| `__mode = "kv"` | Both weak — GC collects either direction |
|
||||
|
||||
**Ephemeron tables (5.2+)**: In Lua 5.2, weak tables with weak keys behave as **ephemeron tables**. In an ephemeron table, a value is considered reachable only if its key is reachable. If the only reference to a key comes through its value (e.g., a table used as both key and value), the entry is removed. This prevents reference cycles from keeping entries alive and is the correct behavior for cache patterns. In 5.0/5.1, a strong value could keep a weak-keyed entry alive even if the key was unreachable.
|
||||
|
||||
**Mode reference**:
|
||||
| Mode | When entry is removed |
|
||||
|------|----------------------|
|
||||
| `"v"` | Value is garbage-collectable and no other references exist |
|
||||
| `"k"` | Key is garbage-collectable and no other references exist |
|
||||
| `"kv"` | Either key or value is independently collectable |
|
||||
@@ -0,0 +1,479 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
---
|
||||
# Lua Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Lua specific content.
|
||||
|
||||
## Mandatory Security Checks (Lua)
|
||||
|
||||
Before ANY commit of Lua code:
|
||||
- [ ] No hardcoded secrets (API keys, passwords, tokens, webhook URLs)
|
||||
- [ ] All user/external inputs validated before processing
|
||||
- [ ] No `loadstring` / `load` with untrusted input
|
||||
- [ ] No `os.execute` / `io.popen` with user-controlled strings
|
||||
- [ ] No unintentional global variables (verified by `luacheck` with `std = "none"`)
|
||||
- [ ] Error messages don't leak file paths, stack traces, or internal state
|
||||
- [ ] SavedVariables don't store sensitive data in plaintext
|
||||
- [ ] Plugin sandbox restricts access to dangerous libraries
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- NEVER hardcode API keys, tokens, or credentials in Lua source files
|
||||
- Use environment variables (`os.getenv("API_KEY")`) for CLI/server Lua
|
||||
- Use secure SavedVariables with obfuscation for addon credentials (if absolutely necessary)
|
||||
- Fail fast if required secrets are missing at startup
|
||||
- Keep `.env` files and SavedVariables files in `.gitignore`
|
||||
|
||||
```lua
|
||||
-- BAD: Hardcoded secret
|
||||
local API_KEY = "sk-abc123secretkey"
|
||||
|
||||
-- GOOD: Environment variable with early validation
|
||||
local function loadApiKey()
|
||||
local key = os.getenv("API_KEY")
|
||||
if not key or key == "" then
|
||||
error("API_KEY environment variable must be set")
|
||||
end
|
||||
return key
|
||||
end
|
||||
```
|
||||
|
||||
## `loadstring` and Dynamic Code Execution
|
||||
|
||||
`loadstring` (Lua 5.0/5.1) / `load` (Lua 5.2+) execute arbitrary strings as code. This is the **single most critical attack surface** in Lua.
|
||||
|
||||
**Version note**: `loadstring` exists in Lua 5.0 and 5.1. In Lua 5.2+, `loadstring` was removed and its functionality was merged into `load`. In Lua 5.0, `loadlib` (not `loadstring`) is also available for loading C libraries — it was moved to `package.loadlib` in 5.1 and later.
|
||||
|
||||
### Never Do This
|
||||
|
||||
```lua
|
||||
-- CRITICAL RISK: Arbitrary code execution from user input
|
||||
local fn = loadstring(userInput)
|
||||
if fn then fn() end
|
||||
|
||||
-- CRITICAL RISK: Loading from untrusted file path
|
||||
local fn = loadfile(userSuppliedPath)
|
||||
if fn then fn() end
|
||||
|
||||
-- CRITICAL RISK: Dynamic code from network data
|
||||
local fn = loadstring(httpResponse.body)
|
||||
```
|
||||
|
||||
### When It Is Acceptable
|
||||
|
||||
- Deserializing data from a **trusted, internal source** (e.g., `AceSerializer` output from your own SavedVariables written by your own addon)
|
||||
- Compile-time / build tooling code that never runs in production
|
||||
- MUST have a `-- SECURITY: loadstring used here because ...` comment
|
||||
|
||||
### Alternatives to loadstring
|
||||
|
||||
| Problem | Use Instead |
|
||||
|---------|-------------|
|
||||
| Dynamic function dispatch | Function lookup tables |
|
||||
| Computed field access | `rawget(table, key)` |
|
||||
| Data deserialization | `AceSerializer`, JSON parser, custom binary format |
|
||||
| Template expansion | `string.format` or `gsub` with controlled patterns |
|
||||
|
||||
```lua
|
||||
-- BAD: Dynamic dispatch via loadstring
|
||||
local fn = loadstring("return " .. actionName .. "()")
|
||||
|
||||
-- GOOD: Function dispatch table
|
||||
local actions = {
|
||||
attack = function() return doAttack() end,
|
||||
defend = function() return doDefend() end,
|
||||
heal = function() return doHeal() end,
|
||||
}
|
||||
local fn = actions[actionName]
|
||||
if fn then fn() end
|
||||
```
|
||||
|
||||
### Bytecode Loading (5.2+ Security Risk)
|
||||
|
||||
Starting in Lua 5.2, bytecode verification was removed. Loading untrusted binary data via `load()` or `loadfile()` can execute arbitrary code even without `loadstring`. **Always restrict to text-only mode when loading untrusted input:**
|
||||
|
||||
```lua
|
||||
-- CRITICAL: restrict to text mode when source is untrusted
|
||||
-- The 4th arg 't' = text only, 'b' = binary only, 'bt' = both (default, unsafe)
|
||||
-- In 5.2: mode is 3rd arg; in 5.3+: mode is 4th arg (after chunk name)
|
||||
local fn, err = load(untrusted_source, "=(untrusted)", nil, "t")
|
||||
|
||||
-- Safe alternative: explicit source validation before loading
|
||||
if not isKnownTrustedSource(source) then
|
||||
return nil, "Refused to load untrusted source"
|
||||
end
|
||||
|
||||
-- In Lua 5.1 and earlier, load() does not accept a mode parameter.
|
||||
-- loadstring() only accepts text, so binary injection is not a risk via loadstring.
|
||||
-- However, loadfile() can load binary chunks in all versions — validate files first.
|
||||
```
|
||||
|
||||
*Source: Lua 5.2 Manual §8.2 — "Lua does not have bytecode verification anymore. So, all functions that load code (load and loadfile) are potentially insecure when loading untrusted binary data."*
|
||||
|
||||
## Global Namespace Leakage
|
||||
|
||||
Every variable written without `local` is an implicit global in Lua. This has severe consequences:
|
||||
|
||||
1. **Cross-addon contamination**: Any addon can read or overwrite your globals
|
||||
2. **Information leak**: Internal data structures become publicly visible
|
||||
3. **Taint**: In WoW, global writes from a tainted call stack propagate taint to the written variable, which then propagates to anything that reads it
|
||||
4. **Silent bugs**: Typos in variable names silently create new globals instead of erroring
|
||||
|
||||
### Prevention
|
||||
|
||||
```lua
|
||||
-- Use luacheck with std = "none" to catch ALL undeclared globals
|
||||
-- See hooks.md for .luacheckrc configuration
|
||||
|
||||
-- BAD: Leaks NPC data into the global namespace
|
||||
QuestieX_WotLKDB_npc = addonTable.npcData -- Visible to every addon!
|
||||
|
||||
-- GOOD: Share via Plugin API (private channel)
|
||||
local plugin = QuestiePluginAPI:RegisterPlugin("WotLKDB")
|
||||
plugin.data = addonTable -- Only accessible through the registry
|
||||
```
|
||||
|
||||
### Runtime Global Access Monitoring (Development)
|
||||
|
||||
For debugging, use a `__newindex` hook on `_G` to detect unexpected global writes:
|
||||
|
||||
```lua
|
||||
-- WARNING: Development only — remove before release
|
||||
if DEBUG_MODE then
|
||||
setmetatable(_G, {
|
||||
__newindex = function(t, k, v)
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
print(string.format(
|
||||
"WARNING: Global write: %s = %s at %s:%d",
|
||||
tostring(k), tostring(v),
|
||||
info.short_src, info.currentline
|
||||
))
|
||||
rawset(t, k, v)
|
||||
end
|
||||
})
|
||||
end
|
||||
```
|
||||
|
||||
## Input Validation at System Boundaries
|
||||
|
||||
Validate all external input — user text, network data, saved variable files, addon communication — before processing:
|
||||
|
||||
```lua
|
||||
-- GOOD: Validate before use, return nil + error for invalid input
|
||||
local function safeGetNPC(npcId)
|
||||
if type(npcId) ~= "number" then
|
||||
return nil, "npcId must be a number, got: " .. type(npcId)
|
||||
end
|
||||
if npcId <= 0 or npcId ~= math.floor(npcId) then
|
||||
return nil, "npcId must be a positive integer, got: " .. tostring(npcId)
|
||||
end
|
||||
return QuestieDB:GetNPC(npcId)
|
||||
end
|
||||
|
||||
-- GOOD: Validate deserialized data structure
|
||||
local function validateConfig(config)
|
||||
if type(config) ~= "table" then return nil, "config must be a table" end
|
||||
if type(config.version) ~= "string" then return nil, "config.version must be a string" end
|
||||
if type(config.maxLevel) ~= "number" then return nil, "config.maxLevel must be a number" end
|
||||
if config.maxLevel < 1 or config.maxLevel > 100 then
|
||||
return nil, "config.maxLevel out of range: " .. config.maxLevel
|
||||
end
|
||||
return config
|
||||
end
|
||||
```
|
||||
|
||||
### SavedVariables Validation
|
||||
|
||||
Always validate SavedVariables on load — they can be manually edited by users or corrupted:
|
||||
|
||||
```lua
|
||||
function Questie:LoadSavedVariables()
|
||||
local sv = QuestieSV
|
||||
if type(sv) ~= "table" then
|
||||
-- Corrupted or missing — reset to defaults
|
||||
QuestieSV = self:GetDefaults()
|
||||
return
|
||||
end
|
||||
|
||||
-- Validate schema version
|
||||
if type(sv.version) ~= "number" or sv.version < MIN_SV_VERSION then
|
||||
-- Schema too old — migrate or reset
|
||||
QuestieSV = self:MigrateSV(sv)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## File I/O (Standalone Lua)
|
||||
|
||||
In sandboxed environments (WoW), `io` is not available. In standalone scripts:
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
```lua
|
||||
-- BAD: User-controlled path — allows directory traversal
|
||||
local f = io.open(userPath, "r")
|
||||
|
||||
-- GOOD: Validate path is within allowed directory
|
||||
local function safeOpen(filename, mode)
|
||||
-- Strip path traversal attempts
|
||||
if filename:find("%.%.") or filename:find("[/\\]") then
|
||||
return nil, "Invalid filename: path traversal detected"
|
||||
end
|
||||
local fullPath = SAFE_DIRECTORY .. "/" .. filename
|
||||
return io.open(fullPath, mode)
|
||||
end
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
|
||||
Always close file handles to prevent resource exhaustion:
|
||||
|
||||
```lua
|
||||
-- BAD: Handle leak on error
|
||||
local f = io.open("data.txt", "r")
|
||||
local content = f:read("*a") -- If this errors, f is never closed
|
||||
f:close()
|
||||
|
||||
-- GOOD: Protected read with guaranteed close
|
||||
local function readFile(path)
|
||||
local f, err = io.open(path, "r")
|
||||
if not f then return nil, err end
|
||||
|
||||
local ok, content = pcall(f.read, f, "*a")
|
||||
f:close() -- Always close, even if read failed
|
||||
|
||||
if not ok then return nil, content end
|
||||
return content
|
||||
end
|
||||
```
|
||||
|
||||
## Shell Injection (`os.execute` / `io.popen`)
|
||||
|
||||
NEVER pass user-controlled strings to shell commands:
|
||||
|
||||
```lua
|
||||
-- CRITICAL RISK: Shell injection
|
||||
os.execute("grep " .. userInput .. " /var/log/app.log")
|
||||
-- An attacker sends: "; rm -rf / #" as userInput
|
||||
|
||||
-- CRITICAL RISK: Same with io.popen
|
||||
local handle = io.popen("curl " .. userUrl)
|
||||
|
||||
-- SAFE: Use validated, sanitized inputs or avoid shell entirely
|
||||
local function safeLookup(word)
|
||||
-- Validate: alphanumeric only
|
||||
if not word:match("^%w+$") then
|
||||
return nil, "Invalid input: must be alphanumeric"
|
||||
end
|
||||
-- Now safe to use in a controlled command
|
||||
return os.execute("grep -w " .. word .. " dictionary.txt")
|
||||
end
|
||||
```
|
||||
|
||||
## Sandbox Design for Plugin Systems
|
||||
|
||||
When building systems that run third-party plugin code, restrict the execution environment:
|
||||
|
||||
### Environment Restriction
|
||||
|
||||
```lua
|
||||
-- Create a restricted environment for plugin execution
|
||||
local function createSandbox()
|
||||
return {
|
||||
-- Safe builtins
|
||||
print = print,
|
||||
pairs = pairs,
|
||||
ipairs = ipairs,
|
||||
next = next,
|
||||
type = type,
|
||||
tostring = tostring,
|
||||
tonumber = tonumber,
|
||||
select = select,
|
||||
unpack = unpack,
|
||||
error = error,
|
||||
pcall = pcall,
|
||||
|
||||
-- Safe libraries (read-only subsets)
|
||||
string = {
|
||||
format = string.format,
|
||||
find = string.find,
|
||||
sub = string.sub,
|
||||
len = string.len,
|
||||
lower = string.lower,
|
||||
upper = string.upper,
|
||||
},
|
||||
table = {
|
||||
insert = table.insert,
|
||||
remove = table.remove,
|
||||
sort = table.sort,
|
||||
concat = table.concat,
|
||||
},
|
||||
math = {
|
||||
floor = math.floor,
|
||||
ceil = math.ceil,
|
||||
min = math.min,
|
||||
max = math.max,
|
||||
abs = math.abs,
|
||||
},
|
||||
|
||||
-- EXPLICITLY EXCLUDED:
|
||||
-- os (shell access, file system)
|
||||
-- io (file system access)
|
||||
-- debug (CRITICAL: debug.getupvalue/setupvalue can read/modify
|
||||
-- any upvalue in any function, bypassing sandbox entirely.
|
||||
-- debug.getlocal/setlocal can read/modify locals on the
|
||||
-- call stack. debug.setmetatable bypasses __metatable guards.
|
||||
-- Never expose ANY debug library function to untrusted code.)
|
||||
-- load (arbitrary code execution)
|
||||
-- loadstring (arbitrary code execution)
|
||||
-- loadfile (arbitrary code execution)
|
||||
-- dofile (arbitrary code execution)
|
||||
-- rawget (bypass metamethod guards)
|
||||
-- rawset (bypass metamethod guards)
|
||||
-- setmetatable (override protections)
|
||||
-- getmetatable (inspect protected tables)
|
||||
}
|
||||
end
|
||||
|
||||
-- Execute plugin code in sandbox
|
||||
-- Version-gated: setfenv for 5.0/5.1, _ENV wrapper for 5.2+
|
||||
local function runInSandbox(code, sandbox)
|
||||
if setfenv then
|
||||
-- Lua 5.0 / 5.1: setfenv directly restricts the environment
|
||||
local fn, err = loadstring(code)
|
||||
if not fn then return nil, "Compile error: " .. err end
|
||||
setfenv(fn, sandbox)
|
||||
return pcall(fn)
|
||||
else
|
||||
-- Lua 5.2+: use load() with custom _ENV
|
||||
-- The 4th arg to load() sets the environment
|
||||
local fn, err = load(code, "=(sandbox)", "t", sandbox)
|
||||
if not fn then return nil, "Compile error: " .. err end
|
||||
return pcall(fn)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why this matters**: `setfenv`/`getfenv` were removed in Lua 5.2. Code that relies on `setfenv` for sandboxing will silently fail or error on 5.2+. Always use the version-gated pattern above.
|
||||
|
||||
### Resource Limiting
|
||||
|
||||
For untrusted code, add execution timeout via `debug.sethook` (available in Lua 5.0+):
|
||||
|
||||
```lua
|
||||
local function runWithTimeout(fn, maxInstructions)
|
||||
maxInstructions = maxInstructions or 1000000
|
||||
local count = 0
|
||||
debug.sethook(function()
|
||||
count = count + 1
|
||||
if count > maxInstructions then
|
||||
error("Execution limit exceeded: suspected infinite loop")
|
||||
end
|
||||
end, "", 1) -- Hook every instruction
|
||||
|
||||
local ok, result = pcall(fn)
|
||||
debug.sethook() -- Remove hook
|
||||
return ok, result
|
||||
end
|
||||
```
|
||||
|
||||
**Version note**: `debug.sethook` is available in Lua 5.0+ and works identically across all versions. The `debug` library itself may be stripped in sandboxed environments (WoW does not expose the full `debug` library to addons).
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- Audit third-party Lua libraries (LuaRocks packages) for known CVEs before vendoring
|
||||
- Pin dependency versions in rockspec or lockfile — never use floating `latest`
|
||||
- Prefer small, auditable libraries over large frameworks for security-sensitive code
|
||||
- Vendor critical dependencies (copy into project) rather than relying on external resolution
|
||||
- Review transitive dependencies: `luarocks show <package>` lists deps
|
||||
|
||||
```bash
|
||||
# List all installed packages and versions
|
||||
luarocks list
|
||||
|
||||
# Show package info including dependencies
|
||||
luarocks show lpeg
|
||||
|
||||
# Install specific version (avoid floating latest)
|
||||
luarocks install luacheck 1.1.2
|
||||
```
|
||||
|
||||
## Error Message Security
|
||||
|
||||
Never expose internal details in user-facing error messages:
|
||||
|
||||
```lua
|
||||
-- BAD: Leaks internal path and database schema
|
||||
error("Failed to load NPC " .. npcId .. " from "
|
||||
.. dbPath .. ": column 'rawdata' is nil at index " .. idx)
|
||||
|
||||
-- GOOD: Generic user message, detailed internal log
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL,
|
||||
format("[QuestieDB] GetNPC failed: npcId=%d, rawdata=nil, source=%s",
|
||||
npcId, dbPath))
|
||||
return nil -- Return nil to caller, no internal details
|
||||
```
|
||||
|
||||
## WoW-Specific: Taint and Secure Code
|
||||
|
||||
Taint is a security mechanism in the WoW client that prevents addon code from executing protected actions (opening bags during combat, using abilities, etc.). Understanding taint is CRITICAL for WoW addon development.
|
||||
|
||||
### Taint Propagation Rules
|
||||
|
||||
1. Any variable written from insecure (addon) code is **tainted**
|
||||
2. Any variable read from tainted state becomes **tainted**
|
||||
3. Taint propagates through function calls, table reads, and variable assignments
|
||||
4. Protected API calls from a tainted call stack trigger `ADDON_ACTION_BLOCKED`
|
||||
|
||||
### Common Taint Sources and Fixes
|
||||
|
||||
| Taint Source | Why It Taints | Fix |
|
||||
|-------------|---------------|-----|
|
||||
| `_G.MyVar = value` | Global write from addon code | Use `addonTable` instead |
|
||||
| `loadstring(code)()` | Compiled code is always tainted | Use function dispatch tables |
|
||||
| Overwriting Blizzard functions | Replaces secure with insecure | Use `hooksecurefunc()` (post-hook) |
|
||||
| Writing from `OnUpdate` | Frequent tainted writes | Move to `ADDON_LOADED` or `PLAYER_LOGIN` |
|
||||
| `rawset(_G, k, v)` | Bypasses metamethods but still taints | Avoid; use local module tables |
|
||||
| Reading a tainted global | Taint propagates to reader | Read during `ADDON_LOADED` or cache locally |
|
||||
|
||||
### Safe WoW API Patterns
|
||||
|
||||
```lua
|
||||
-- GOOD: Post-hook (does not replace the original, does not taint)
|
||||
hooksecurefunc("QuestLogFrame_Update", function()
|
||||
-- Your code runs AFTER the original — cannot taint it
|
||||
end)
|
||||
|
||||
-- BAD: Function replacement (replaces secure with insecure = taint)
|
||||
local original = QuestLogFrame_Update
|
||||
QuestLogFrame_Update = function(...) -- Now tainted!
|
||||
original(...)
|
||||
myCustomLogic()
|
||||
end
|
||||
|
||||
-- GOOD: Combat guard for protected actions
|
||||
local function safeAction()
|
||||
if InCombatLockdown() then
|
||||
-- Queue for after combat
|
||||
return
|
||||
end
|
||||
-- Safe to call protected APIs
|
||||
end
|
||||
```
|
||||
|
||||
## Security Response Protocol
|
||||
|
||||
If a security issue is found in Lua code:
|
||||
1. **STOP** immediately — do not ship the code
|
||||
2. Use **security-reviewer** agent
|
||||
3. Fix CRITICAL issues before continuing
|
||||
4. If secrets were exposed, rotate them immediately
|
||||
5. Audit the entire codebase for similar patterns
|
||||
6. Add `luacheck` rules to prevent recurrence
|
||||
7. Add regression tests for the specific vulnerability
|
||||
|
||||
## References
|
||||
|
||||
See skill: `security-review` for general security checklists applicable across all languages.
|
||||
@@ -0,0 +1,397 @@
|
||||
---
|
||||
paths:
|
||||
- "**/tests/**/*.lua"
|
||||
- "**/*_spec.lua"
|
||||
- "**/*_test.lua"
|
||||
---
|
||||
# Lua Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Lua specific content.
|
||||
|
||||
## Test Frameworks
|
||||
|
||||
| Framework | Best for | Install |
|
||||
|-----------|---------|---------|
|
||||
| **Busted** | BDD-style unit/integration testing (most popular) | `luarocks install busted` |
|
||||
| **LuaUnit** | xUnit-style, zero dependencies, minimal footprint | `luarocks install luaunit` |
|
||||
| **Telescope** | Flexible, extensible, custom reporters | `luarocks install telescope` |
|
||||
| **WoW mocks** | Game addon testing with C_API stubs | Custom `tests/mocks/` |
|
||||
|
||||
**Default choice**: Use **Busted** unless the project has an existing convention.
|
||||
|
||||
## Test Organization
|
||||
|
||||
```text
|
||||
project/
|
||||
├── src/ # Production code
|
||||
│ ├── Database/
|
||||
│ │ └── QuestieDB.lua
|
||||
│ └── Modules/
|
||||
│ └── QuestieInit.lua
|
||||
├── tests/
|
||||
│ ├── mocks/ # Environment stubs
|
||||
│ │ ├── wow_api.lua # CreateFrame, C_Timer, GetTime stubs
|
||||
│ │ ├── questie_env.lua # QuestieLoader, Questie object stubs
|
||||
│ │ └── saved_vars.lua # SavedVariables mock
|
||||
│ ├── unit/ # Unit tests (one per module)
|
||||
│ │ ├── QuestieDB_spec.lua
|
||||
│ │ ├── ZoneDB_spec.lua
|
||||
│ │ └── compiler_spec.lua
|
||||
│ ├── integration/ # Multi-module interaction tests
|
||||
│ │ └── init_flow_spec.lua
|
||||
│ └── helpers/ # Shared test utilities
|
||||
│ ├── assertions.lua # Custom assert functions
|
||||
│ └── fixtures.lua # Reusable test data
|
||||
├── .busted # Busted configuration
|
||||
└── .luacov # Coverage configuration
|
||||
```
|
||||
|
||||
- Name spec files with `_spec.lua` suffix (Busted convention) or `_test.lua`
|
||||
- Mirror the source directory structure in `tests/unit/`
|
||||
- One spec file per production module
|
||||
|
||||
## Busted Configuration (`.busted`)
|
||||
|
||||
```lua
|
||||
return {
|
||||
_all = {
|
||||
coverage = false,
|
||||
lpath = "src/?.lua;src/?/init.lua",
|
||||
},
|
||||
default = {
|
||||
verbose = true,
|
||||
output = "utfTerminal",
|
||||
ROOT = { "tests/" },
|
||||
},
|
||||
ci = {
|
||||
ROOT = { "tests/" },
|
||||
output = "TAP",
|
||||
coverage = true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Unit Test Patterns
|
||||
|
||||
### Basic describe / it / before / after
|
||||
|
||||
```lua
|
||||
describe("QuestieDB", function()
|
||||
local QuestieDB
|
||||
|
||||
before_each(function()
|
||||
-- Fresh module instance per test (isolation)
|
||||
package.loaded["Database.QuestieDB"] = nil
|
||||
QuestieDB = require("Database.QuestieDB")
|
||||
QuestieDB.npcData = {
|
||||
[1] = { "Hogger", 11, 0 },
|
||||
[26680] = { "Grizzly Hills NPC", 74, 1 },
|
||||
}
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
package.loaded["Database.QuestieDB"] = nil
|
||||
end)
|
||||
|
||||
describe(":GetNPC", function()
|
||||
it("returns NPC data for a valid id", function()
|
||||
local data = QuestieDB:GetNPC(1)
|
||||
assert.is_not_nil(data)
|
||||
assert.are.equal("Hogger", data.name)
|
||||
end)
|
||||
|
||||
it("returns nil for an unknown id", function()
|
||||
assert.is_nil(QuestieDB:GetNPC(99999))
|
||||
end)
|
||||
|
||||
it("returns nil for nil input", function()
|
||||
assert.is_nil(QuestieDB:GetNPC(nil))
|
||||
end)
|
||||
|
||||
it("returns nil for non-numeric input", function()
|
||||
assert.is_nil(QuestieDB:GetNPC("abc"))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
### Pending Tests (Work-in-Progress)
|
||||
|
||||
Mark incomplete tests with `pending` — they appear in reports but do not fail:
|
||||
|
||||
```lua
|
||||
pending("respects NPC blacklist during query")
|
||||
pending("handles WotLK-only NPCs when plugin is absent")
|
||||
```
|
||||
|
||||
### Test Naming Conventions
|
||||
|
||||
Use descriptive names that explain the scenario and expected outcome:
|
||||
|
||||
```lua
|
||||
-- GOOD: Explains what + when + expected outcome
|
||||
it("returns nil when NPC id does not exist in the database", ...)
|
||||
it("triggers recompile when WotLK plugin is newly installed", ...)
|
||||
it("skips cleanup when database has not been compiled yet", ...)
|
||||
|
||||
-- BAD: Vague
|
||||
it("works", ...)
|
||||
it("test 1", ...)
|
||||
```
|
||||
|
||||
## Assertions Reference (Busted)
|
||||
|
||||
| Assertion | Meaning |
|
||||
|-----------|---------|
|
||||
| `assert.are.equal(expected, actual)` | Strict equality (`==`) |
|
||||
| `assert.are.same(t1, t2)` | Deep table value equality |
|
||||
| `assert.is_nil(v)` | `v == nil` |
|
||||
| `assert.is_not_nil(v)` | `v ~= nil` |
|
||||
| `assert.is_true(v)` | `v == true` (strict, not truthy) |
|
||||
| `assert.is_false(v)` | `v == false` (strict, not falsy) |
|
||||
| `assert.truthy(v)` | `v` is truthy (not `nil`/`false`) |
|
||||
| `assert.falsy(v)` | `v` is falsy (`nil` or `false`) |
|
||||
| `assert.has_error(fn)` | `fn()` throws any error |
|
||||
| `assert.has_error(fn, "msg")` | `fn()` throws with specific message |
|
||||
| `assert.has_no_error(fn)` | `fn()` does not throw |
|
||||
| `assert.are.near(expected, actual, tolerance)` | Float comparison within epsilon |
|
||||
|
||||
## Mocking and Spies
|
||||
|
||||
### spy.on — Observe without replacing
|
||||
|
||||
```lua
|
||||
local s = spy.on(Questie, "Debug")
|
||||
QuestieDB:GetNPC(0)
|
||||
assert.spy(s).was.called()
|
||||
assert.spy(s).was.called_with(
|
||||
match._, -- self (Questie)
|
||||
Questie.DEBUG_CRITICAL, -- severity
|
||||
match._ -- message string
|
||||
)
|
||||
s:revert() -- Restore original (automatic in after_each)
|
||||
```
|
||||
|
||||
### stub — Replace with controlled implementation
|
||||
|
||||
```lua
|
||||
stub(QuestieDB, "QueryNPCSingle").returns(nil)
|
||||
|
||||
-- Verify it was called with specific args
|
||||
assert.stub(QuestieDB.QueryNPCSingle).was.called_with(
|
||||
match._, 26680, match._
|
||||
)
|
||||
|
||||
QuestieDB.QueryNPCSingle:revert()
|
||||
```
|
||||
|
||||
### mock — Full module replacement
|
||||
|
||||
```lua
|
||||
local mockDB = mock({
|
||||
GetNPC = function(_, id) return { name = "Mock NPC " .. id } end,
|
||||
GetQuest = function() return nil end,
|
||||
})
|
||||
```
|
||||
|
||||
**Rule**: Always restore stubs and spies. Busted auto-reverts stubs created with `stub()` at the end of each `it()` block, but manually-created stubs need explicit `:revert()`.
|
||||
|
||||
## WoW API Mock Environment
|
||||
|
||||
Create a `tests/mocks/wow_api.lua` that defines the WoW protected API surface. Require it BEFORE any addon file:
|
||||
|
||||
```lua
|
||||
-- tests/mocks/wow_api.lua
|
||||
|
||||
-- Frame system
|
||||
_G.CreateFrame = function(frameType, name, parent, template)
|
||||
local frame = {
|
||||
_events = {},
|
||||
_scripts = {},
|
||||
RegisterEvent = function(self, event) self._events[event] = true end,
|
||||
UnregisterEvent = function(self, event) self._events[event] = nil end,
|
||||
SetScript = function(self, handler, fn) self._scripts[handler] = fn end,
|
||||
GetScript = function(self, handler) return self._scripts[handler] end,
|
||||
Show = function() end,
|
||||
Hide = function() end,
|
||||
IsShown = function() return true end,
|
||||
}
|
||||
return frame
|
||||
end
|
||||
|
||||
-- Timer system
|
||||
_G.C_Timer = {
|
||||
After = function(delay, fn) fn() end, -- Execute immediately in tests
|
||||
NewTicker = function(interval, fn, iterations)
|
||||
for i = 1, (iterations or 1) do fn() end
|
||||
return { Cancel = function() end }
|
||||
end,
|
||||
}
|
||||
|
||||
-- Time
|
||||
_G.GetTime = function() return os.clock() end
|
||||
_G.time = os.time
|
||||
|
||||
-- Unit info
|
||||
_G.UnitGUID = function(unit) return "Player-1234-ABCDEF" end
|
||||
_G.UnitName = function(unit) return "TestPlayer" end
|
||||
_G.UnitLevel = function(unit) return 80 end
|
||||
_G.UnitFactionGroup = function(unit) return "Alliance", "Alliance" end
|
||||
|
||||
-- Map API
|
||||
_G.C_Map = {
|
||||
GetMapInfo = function(mapID) return { mapID = mapID, name = "Test Zone" } end,
|
||||
GetBestMapForUnit = function(unit) return 1 end,
|
||||
}
|
||||
|
||||
-- Enum system
|
||||
_G.Enum = {
|
||||
UIMapType = { Cosmic = 0, World = 1, Continent = 2, Zone = 3, Dungeon = 4 },
|
||||
}
|
||||
|
||||
-- Misc
|
||||
_G.IsAddOnLoaded = function(name) return true end
|
||||
_G.GetAddOnInfo = function(name) return name, "Test Addon", "", true, "INSECURE" end
|
||||
_G.InCombatLockdown = function() return false end
|
||||
_G.debugstack = function(level) return "mock stack trace" end
|
||||
_G.geterrorhandler = function() return print end
|
||||
_G.hooksecurefunc = function(table, name, fn) end
|
||||
_G.print = print
|
||||
_G.wipe = function(t) for k in pairs(t) do t[k] = nil end return t end
|
||||
_G.select = select
|
||||
_G.format = string.format
|
||||
_G.strsplit = function(sep, str)
|
||||
local parts = {}
|
||||
local pattern = "([^" .. sep .. "]+)"
|
||||
for match in str:gmatch(pattern) do
|
||||
table.insert(parts, match)
|
||||
end
|
||||
return unpack(parts)
|
||||
end
|
||||
|
||||
-- SavedVariables
|
||||
_G.QuestieSV = {}
|
||||
```
|
||||
|
||||
### Firing Events in Tests
|
||||
|
||||
```lua
|
||||
-- Helper to fire a WoW event on a frame mock
|
||||
local function fireEvent(frame, event, ...)
|
||||
if frame._events[event] then
|
||||
local onEvent = frame._scripts["OnEvent"]
|
||||
if onEvent then
|
||||
onEvent(frame, event, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Usage in test
|
||||
it("handles PLAYER_LOGIN event", function()
|
||||
local frame = CreateFrame("Frame")
|
||||
frame:RegisterEvent("PLAYER_LOGIN")
|
||||
frame:SetScript("OnEvent", myHandler)
|
||||
fireEvent(frame, "PLAYER_LOGIN")
|
||||
-- assert expected side effects
|
||||
end)
|
||||
```
|
||||
|
||||
## Error Path Testing
|
||||
|
||||
**Always test error paths, not just happy paths:**
|
||||
|
||||
```lua
|
||||
describe("error handling", function()
|
||||
it("handles nil rawdata gracefully", function()
|
||||
QuestieDB.npcData = {}
|
||||
local result = QuestieDB:GetNPC(99999)
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it("logs critical error for nil rawdata", function()
|
||||
local s = spy.on(Questie, "Debug")
|
||||
QuestieDB.npcData = {}
|
||||
QuestieDB:GetNPC(99999)
|
||||
assert.spy(s).was.called_with(
|
||||
match._, Questie.DEBUG_CRITICAL, match.is_string()
|
||||
)
|
||||
end)
|
||||
|
||||
it("survives pcall on corrupted data", function()
|
||||
QuestieDB.npcData = { [1] = "not a table" }
|
||||
assert.has_no_error(function()
|
||||
QuestieDB:GetNPC(1)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
Use **Luacov** for line coverage. Target ≥ 80%.
|
||||
|
||||
### Configuration (`.luacov`)
|
||||
|
||||
```lua
|
||||
return {
|
||||
statsfile = "luacov.stats.out",
|
||||
reportfile = "luacov.report.out",
|
||||
exclude = {
|
||||
"tests/", -- Exclude test files from coverage
|
||||
"Database/Data/.*Data", -- Exclude large static data tables
|
||||
"Localization/Translations/", -- Exclude translation strings
|
||||
},
|
||||
include = {
|
||||
"Database/",
|
||||
"Modules/",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
busted --coverage
|
||||
|
||||
# Generate report
|
||||
luacov
|
||||
cat luacov.report.out
|
||||
|
||||
# Fail CI if below threshold
|
||||
awk '/^Total/ { if ($4+0 < 80) { print "Coverage below 80%: " $4 "%"; exit 1 } }' luacov.report.out
|
||||
```
|
||||
|
||||
## Test-Driven Development Workflow (Lua)
|
||||
|
||||
1. **RED** — Write the test first. It must FAIL.
|
||||
2. **GREEN** — Write the minimum production code to make it pass.
|
||||
3. **REFACTOR** — Clean up while keeping tests green.
|
||||
4. **COVERAGE** — Verify ≥ 80% with `busted --coverage && luacov`.
|
||||
|
||||
```bash
|
||||
# TDD cycle
|
||||
busted tests/unit/QuestieDB_spec.lua # RED: expect failures
|
||||
# ... write implementation ...
|
||||
busted tests/unit/QuestieDB_spec.lua # GREEN: expect passes
|
||||
# ... refactor ...
|
||||
busted --coverage && luacov # COVERAGE: verify 80%+
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
busted # Run all tests
|
||||
busted --verbose # Verbose output
|
||||
busted tests/unit/ # Run only unit tests
|
||||
busted --filter="GetNPC" # Run tests matching pattern
|
||||
busted --tags="wotlk" # Run tagged tests only
|
||||
busted --coverage # With coverage collection
|
||||
busted --output=TAP # TAP format for CI
|
||||
busted --shuffle # Randomize test order (detect coupling)
|
||||
```
|
||||
|
||||
## Agent Support
|
||||
|
||||
- **tdd-guide** — Use proactively for new features; enforces write-tests-first workflow
|
||||
- **code-reviewer** — Review test quality after writing tests
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Perl-specific content.
|
||||
|
||||
## Standards
|
||||
|
||||
- Always `use v5.36` (enables `strict`, `warnings`, `say`, subroutine signatures)
|
||||
- Use subroutine signatures — never unpack `@_` manually
|
||||
- Prefer `say` over `print` with explicit newlines
|
||||
|
||||
## Immutability
|
||||
|
||||
- Use **Moo** with `is => 'ro'` and `Types::Standard` for all attributes
|
||||
- Never use blessed hashrefs directly — always use Moo/Moose accessors
|
||||
- **OO override note**: Moo `has` attributes with `builder` or `default` are acceptable for computed read-only values
|
||||
|
||||
## Formatting
|
||||
|
||||
Use **perltidy** with these settings:
|
||||
|
||||
```
|
||||
-i=4 # 4-space indent
|
||||
-l=100 # 100 char line length
|
||||
-ce # cuddled else
|
||||
-bar # opening brace always right
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
Use **perlcritic** at severity 3 with themes: `core`, `pbp`, `security`.
|
||||
|
||||
```bash
|
||||
perlcritic --severity 3 --theme 'core || pbp || security' lib/
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `perl-patterns` for comprehensive modern Perl idioms and best practices.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Perl-specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **perltidy**: Auto-format `.pl` and `.pm` files after edit
|
||||
- **perlcritic**: Run lint check after editing `.pm` files
|
||||
|
||||
## Warnings
|
||||
|
||||
- Warn about `print` in non-script `.pm` files — use `say` or a logging module (e.g., `Log::Any`)
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Perl-specific content.
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
Use **DBI** or **DBIx::Class** behind an interface:
|
||||
|
||||
```perl
|
||||
package MyApp::Repo::User;
|
||||
use Moo;
|
||||
|
||||
has dbh => (is => 'ro', required => 1);
|
||||
|
||||
sub find_by_id ($self, $id) {
|
||||
my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?');
|
||||
$sth->execute($id);
|
||||
return $sth->fetchrow_hashref;
|
||||
}
|
||||
```
|
||||
|
||||
## DTOs / Value Objects
|
||||
|
||||
Use **Moo** classes with **Types::Standard** (equivalent to Python dataclasses):
|
||||
|
||||
```perl
|
||||
package MyApp::DTO::User;
|
||||
use Moo;
|
||||
use Types::Standard qw(Str Int);
|
||||
|
||||
has name => (is => 'ro', isa => Str, required => 1);
|
||||
has email => (is => 'ro', isa => Str, required => 1);
|
||||
has age => (is => 'ro', isa => Int);
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
- Always use **three-arg open** with `autodie`
|
||||
- Use **Path::Tiny** for file operations
|
||||
|
||||
```perl
|
||||
use autodie;
|
||||
use Path::Tiny;
|
||||
|
||||
my $content = path('config.json')->slurp_utf8;
|
||||
```
|
||||
|
||||
## Module Interface
|
||||
|
||||
Use `Exporter 'import'` with `@EXPORT_OK` — never `@EXPORT`:
|
||||
|
||||
```perl
|
||||
use Exporter 'import';
|
||||
our @EXPORT_OK = qw(parse_config validate_input);
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
Use **cpanfile** + **carton** for reproducible installs:
|
||||
|
||||
```bash
|
||||
carton install
|
||||
carton exec prove -lr t/
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `perl-patterns` for comprehensive modern Perl patterns and idioms.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Perl-specific content.
|
||||
|
||||
## Taint Mode
|
||||
|
||||
- Use `-T` flag on all CGI/web-facing scripts
|
||||
- Sanitize `%ENV` (`$ENV{PATH}`, `$ENV{CDPATH}`, etc.) before any external command
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Use allowlist regex for untainting — never `/(.*)/s`
|
||||
- Validate all user input with explicit patterns:
|
||||
|
||||
```perl
|
||||
if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) {
|
||||
my $clean = $1;
|
||||
}
|
||||
```
|
||||
|
||||
## File I/O
|
||||
|
||||
- **Three-arg open only** — never two-arg open
|
||||
- Prevent path traversal with `Cwd::realpath`:
|
||||
|
||||
```perl
|
||||
use Cwd 'realpath';
|
||||
my $safe_path = realpath($user_path);
|
||||
die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/};
|
||||
```
|
||||
|
||||
## Process Execution
|
||||
|
||||
- Use **list-form `system()`** — never single-string form
|
||||
- Use **IPC::Run3** for capturing output
|
||||
- Never use backticks with variable interpolation
|
||||
|
||||
```perl
|
||||
system('grep', '-r', $pattern, $directory); # safe
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
Always use DBI placeholders — never interpolate into SQL:
|
||||
|
||||
```perl
|
||||
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
|
||||
$sth->execute($email);
|
||||
```
|
||||
|
||||
## Security Scanning
|
||||
|
||||
Run **perlcritic** with the security theme at severity 4+:
|
||||
|
||||
```bash
|
||||
perlcritic --severity 4 --theme security lib/
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `perl-security` for comprehensive Perl security patterns, taint mode, and safe I/O.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Perl-specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use **Test2::V0** for new projects (not Test::More):
|
||||
|
||||
```perl
|
||||
use Test2::V0;
|
||||
|
||||
is($result, 42, 'answer is correct');
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
## Runner
|
||||
|
||||
```bash
|
||||
prove -l t/ # adds lib/ to @INC
|
||||
prove -lr -j8 t/ # recursive, 8 parallel jobs
|
||||
```
|
||||
|
||||
Always use `-l` to ensure `lib/` is on `@INC`.
|
||||
|
||||
## Coverage
|
||||
|
||||
Use **Devel::Cover** — target 80%+:
|
||||
|
||||
```bash
|
||||
cover -test
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
- **Test::MockModule** — mock methods on existing modules
|
||||
- **Test::MockObject** — create test doubles from scratch
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Always end test files with `done_testing`
|
||||
- Never forget the `-l` flag with `prove`
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `perl-testing` for detailed Perl TDD patterns with Test2::V0, prove, and Devel::Cover.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with PHP specific content.
|
||||
|
||||
## Standards
|
||||
|
||||
- Follow **PSR-12** formatting and naming conventions.
|
||||
- Prefer `declare(strict_types=1);` in application code.
|
||||
- Use scalar type hints, return types, and typed properties everywhere new code permits.
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer immutable DTOs and value objects for data crossing service boundaries.
|
||||
- Use `readonly` properties or immutable constructors for request/response payloads where possible.
|
||||
- Keep arrays for simple maps; promote business-critical structures into explicit classes.
|
||||
|
||||
## Formatting
|
||||
|
||||
- Use **PHP-CS-Fixer** or **Laravel Pint** for formatting.
|
||||
- Use **PHPStan** or **Psalm** for static analysis.
|
||||
- Keep Composer scripts checked in so the same commands run locally and in CI.
|
||||
|
||||
## Imports
|
||||
|
||||
- Add `use` statements for all referenced classes, interfaces, and traits.
|
||||
- Avoid relying on the global namespace unless the project explicitly prefers fully qualified names.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code.
|
||||
- Convert framework/request input into validated DTOs before it reaches domain logic.
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `backend-patterns` for broader service/repository layering guidance.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
- "**/phpstan.neon"
|
||||
- "**/phpstan.neon.dist"
|
||||
- "**/psalm.xml"
|
||||
---
|
||||
# PHP Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with PHP specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **Pint / PHP-CS-Fixer**: Auto-format edited `.php` files.
|
||||
- **PHPStan / Psalm**: Run static analysis after PHP edits in typed codebases.
|
||||
- **PHPUnit / Pest**: Run targeted tests for touched files or modules when edits affect behavior.
|
||||
|
||||
## Warnings
|
||||
|
||||
- Warn on `var_dump`, `dd`, `dump`, or `die()` left in edited files.
|
||||
- Warn when edited PHP files add raw SQL or disable CSRF/session protections.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with PHP specific content.
|
||||
|
||||
## Thin Controllers, Explicit Services
|
||||
|
||||
- Keep controllers focused on transport: auth, validation, serialization, status codes.
|
||||
- Move business rules into application/domain services that are easy to test without HTTP bootstrapping.
|
||||
|
||||
## DTOs and Value Objects
|
||||
|
||||
- Replace shape-heavy associative arrays with DTOs for requests, commands, and external API payloads.
|
||||
- Use value objects for money, identifiers, date ranges, and other constrained concepts.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
- Depend on interfaces or narrow service contracts, not framework globals.
|
||||
- Pass collaborators through constructors so services are testable without service-locator lookups.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Isolate ORM models from domain decisions when the model layer is doing more than persistence.
|
||||
- Wrap third-party SDKs behind small adapters so the rest of the codebase depends on your contract, not theirs.
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `api-design` for endpoint conventions and response-shape guidance.
|
||||
See skill: `laravel-patterns` for Laravel-specific architecture guidance.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.lock"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with PHP specific content.
|
||||
|
||||
## Input and Output
|
||||
|
||||
- Validate request input at the framework boundary (`FormRequest`, Symfony Validator, or explicit DTO validation).
|
||||
- Escape output in templates by default; treat raw HTML rendering as an exception that must be justified.
|
||||
- Never trust query params, cookies, headers, or uploaded file metadata without validation.
|
||||
|
||||
## Database Safety
|
||||
|
||||
- Use prepared statements (`PDO`, Doctrine, Eloquent query builder) for all dynamic queries.
|
||||
- Avoid string-building SQL in controllers/views.
|
||||
- Scope ORM mass-assignment carefully and whitelist writable fields.
|
||||
|
||||
## Secrets and Dependencies
|
||||
|
||||
- Load secrets from environment variables or a secret manager, never from committed config files.
|
||||
- Run `composer audit` in CI and review new package maintainer trust before adding dependencies.
|
||||
- Pin major versions deliberately and remove abandoned packages quickly.
|
||||
|
||||
## Auth and Session Safety
|
||||
|
||||
- Use `password_hash()` / `password_verify()` for password storage.
|
||||
- Regenerate session identifiers after authentication and privilege changes.
|
||||
- Enforce CSRF protection on state-changing web requests.
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `laravel-security` for Laravel-specific security guidance.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/phpunit.xml"
|
||||
- "**/phpunit.xml.dist"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with PHP specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use **PHPUnit** as the default test framework. If **Pest** is configured in the project, prefer Pest for new tests and avoid mixing frameworks.
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit --coverage-text
|
||||
# or
|
||||
vendor/bin/pest --coverage
|
||||
```
|
||||
|
||||
Prefer **pcov** or **Xdebug** in CI, and keep coverage thresholds in CI rather than as tribal knowledge.
|
||||
|
||||
## Test Organization
|
||||
|
||||
- Separate fast unit tests from framework/database integration tests.
|
||||
- Use factory/builders for fixtures instead of large hand-written arrays.
|
||||
- Keep HTTP/controller tests focused on transport and validation; move business rules into service-level tests.
|
||||
|
||||
## Inertia
|
||||
|
||||
If the project uses Inertia.js, prefer `assertInertia` with `AssertableInertia` to verify component names and props instead of raw JSON assertions.
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
|
||||
See skill: `laravel-tdd` for Laravel-specific testing patterns (PHPUnit and Pest).
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Python specific content.
|
||||
|
||||
## Standards
|
||||
|
||||
- Follow **PEP 8** conventions
|
||||
- Use **type annotations** on all function signatures
|
||||
|
||||
## Immutability
|
||||
|
||||
Prefer immutable data structures:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class User:
|
||||
name: str
|
||||
email: str
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: float
|
||||
y: float
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
- **black** for code formatting
|
||||
- **isort** for import sorting
|
||||
- **ruff** for linting
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `python-patterns` for comprehensive Python idioms and patterns.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Python specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **black/ruff**: Auto-format `.py` files after edit
|
||||
- **mypy/pyright**: Run type checking after editing `.py` files
|
||||
|
||||
## Warnings
|
||||
|
||||
- Warn about `print()` statements in edited files (use `logging` module instead)
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Python specific content.
|
||||
|
||||
## Protocol (Duck Typing)
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class Repository(Protocol):
|
||||
def find_by_id(self, id: str) -> dict | None: ...
|
||||
def save(self, entity: dict) -> dict: ...
|
||||
```
|
||||
|
||||
## Dataclasses as DTOs
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class CreateUserRequest:
|
||||
name: str
|
||||
email: str
|
||||
age: int | None = None
|
||||
```
|
||||
|
||||
## Context Managers & Generators
|
||||
|
||||
- Use context managers (`with` statement) for resource management
|
||||
- Use generators for lazy evaluation and memory-efficient iteration
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `python-patterns` for comprehensive patterns including decorators, concurrency, and package organization.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Python specific content.
|
||||
|
||||
## Secret Management
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.environ["OPENAI_API_KEY"] # Raises KeyError if missing
|
||||
```
|
||||
|
||||
## Security Scanning
|
||||
|
||||
- Use **bandit** for static security analysis:
|
||||
```bash
|
||||
bandit -r src/
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `django-security` for Django-specific security guidelines (if applicable).
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Python specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use **pytest** as the testing framework.
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
pytest --cov=src --cov-report=term-missing
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
Use `pytest.mark` for test categorization:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_calculate_total():
|
||||
...
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_database_connection():
|
||||
...
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `python-testing` for detailed pytest patterns and fixtures.
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Rust-specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **rustfmt** for enforcement — always run `cargo fmt` before committing
|
||||
- **clippy** for lints — `cargo clippy -- -D warnings` (treat warnings as errors)
|
||||
- 4-space indent (rustfmt default)
|
||||
- Max line width: 100 characters (rustfmt default)
|
||||
|
||||
## Immutability
|
||||
|
||||
Rust variables are immutable by default — embrace this:
|
||||
|
||||
- Use `let` by default; only use `let mut` when mutation is required
|
||||
- Prefer returning new values over mutating in place
|
||||
- Use `Cow<'_, T>` when a function may or may not need to allocate
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// GOOD — immutable by default, new value returned
|
||||
fn normalize(input: &str) -> Cow<'_, str> {
|
||||
if input.contains(' ') {
|
||||
Cow::Owned(input.replace(' ', "_"))
|
||||
} else {
|
||||
Cow::Borrowed(input)
|
||||
}
|
||||
}
|
||||
|
||||
// BAD — unnecessary mutation
|
||||
fn normalize_bad(input: &mut String) {
|
||||
*input = input.replace(' ', "_");
|
||||
}
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
Follow standard Rust conventions:
|
||||
- `snake_case` for functions, methods, variables, modules, crates
|
||||
- `PascalCase` (UpperCamelCase) for types, traits, enums, type parameters
|
||||
- `SCREAMING_SNAKE_CASE` for constants and statics
|
||||
- Lifetimes: short lowercase (`'a`, `'de`) — descriptive names for complex cases (`'input`)
|
||||
|
||||
## Ownership and Borrowing
|
||||
|
||||
- Borrow (`&T`) by default; take ownership only when you need to store or consume
|
||||
- Never clone to satisfy the borrow checker without understanding the root cause
|
||||
- Accept `&str` over `String`, `&[T]` over `Vec<T>` in function parameters
|
||||
- Use `impl Into<String>` for constructors that need to own a `String`
|
||||
|
||||
```rust
|
||||
// GOOD — borrows when ownership isn't needed
|
||||
fn word_count(text: &str) -> usize {
|
||||
text.split_whitespace().count()
|
||||
}
|
||||
|
||||
// GOOD — takes ownership in constructor via Into
|
||||
fn new(name: impl Into<String>) -> Self {
|
||||
Self { name: name.into() }
|
||||
}
|
||||
|
||||
// BAD — takes String when &str suffices
|
||||
fn word_count_bad(text: String) -> usize {
|
||||
text.split_whitespace().count()
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Use `Result<T, E>` and `?` for propagation — never `unwrap()` in production code
|
||||
- **Libraries**: define typed errors with `thiserror`
|
||||
- **Applications**: use `anyhow` for flexible error context
|
||||
- Add context with `.with_context(|| format!("failed to ..."))?`
|
||||
- Reserve `unwrap()` / `expect()` for tests and truly unreachable states
|
||||
|
||||
```rust
|
||||
// GOOD — library error with thiserror
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to read config: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid config format: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
// GOOD — application error with anyhow
|
||||
use anyhow::Context;
|
||||
|
||||
fn load_config(path: &str) -> anyhow::Result<Config> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {path}"))?;
|
||||
toml::from_str(&content)
|
||||
.with_context(|| format!("failed to parse {path}"))
|
||||
}
|
||||
```
|
||||
|
||||
## Iterators Over Loops
|
||||
|
||||
Prefer iterator chains for transformations; use loops for complex control flow:
|
||||
|
||||
```rust
|
||||
// GOOD — declarative and composable
|
||||
let active_emails: Vec<&str> = users.iter()
|
||||
.filter(|u| u.is_active)
|
||||
.map(|u| u.email.as_str())
|
||||
.collect();
|
||||
|
||||
// GOOD — loop for complex logic with early returns
|
||||
for user in &users {
|
||||
if let Some(verified) = verify_email(&user.email)? {
|
||||
send_welcome(&verified)?;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module Organization
|
||||
|
||||
Organize by domain, not by type:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── main.rs
|
||||
├── lib.rs
|
||||
├── auth/ # Domain module
|
||||
│ ├── mod.rs
|
||||
│ ├── token.rs
|
||||
│ └── middleware.rs
|
||||
├── orders/ # Domain module
|
||||
│ ├── mod.rs
|
||||
│ ├── model.rs
|
||||
│ └── service.rs
|
||||
└── db/ # Infrastructure
|
||||
├── mod.rs
|
||||
└── pool.rs
|
||||
```
|
||||
|
||||
## Visibility
|
||||
|
||||
- Default to private; use `pub(crate)` for internal sharing
|
||||
- Only mark `pub` what is part of the crate's public API
|
||||
- Re-export public API from `lib.rs`
|
||||
|
||||
## References
|
||||
|
||||
See skill: `rust-patterns` for comprehensive Rust idioms and patterns.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
- "**/Cargo.toml"
|
||||
---
|
||||
# Rust Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Rust-specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **cargo fmt**: Auto-format `.rs` files after edit
|
||||
- **cargo clippy**: Run lint checks after editing Rust files
|
||||
- **cargo check**: Verify compilation after changes (faster than `cargo build`)
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Rust-specific content.
|
||||
|
||||
## Repository Pattern with Traits
|
||||
|
||||
Encapsulate data access behind a trait:
|
||||
|
||||
```rust
|
||||
pub trait OrderRepository: Send + Sync {
|
||||
fn find_by_id(&self, id: u64) -> Result<Option<Order>, StorageError>;
|
||||
fn find_all(&self) -> Result<Vec<Order>, StorageError>;
|
||||
fn save(&self, order: &Order) -> Result<Order, StorageError>;
|
||||
fn delete(&self, id: u64) -> Result<(), StorageError>;
|
||||
}
|
||||
```
|
||||
|
||||
Concrete implementations handle storage details (Postgres, SQLite, in-memory for tests).
|
||||
|
||||
## Service Layer
|
||||
|
||||
Business logic in service structs; inject dependencies via constructor:
|
||||
|
||||
```rust
|
||||
pub struct OrderService {
|
||||
repo: Box<dyn OrderRepository>,
|
||||
payment: Box<dyn PaymentGateway>,
|
||||
}
|
||||
|
||||
impl OrderService {
|
||||
pub fn new(repo: Box<dyn OrderRepository>, payment: Box<dyn PaymentGateway>) -> Self {
|
||||
Self { repo, payment }
|
||||
}
|
||||
|
||||
pub fn place_order(&self, request: CreateOrderRequest) -> anyhow::Result<OrderSummary> {
|
||||
let order = Order::from(request);
|
||||
self.payment.charge(order.total())?;
|
||||
let saved = self.repo.save(&order)?;
|
||||
Ok(OrderSummary::from(saved))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Newtype Pattern for Type Safety
|
||||
|
||||
Prevent argument mix-ups with distinct wrapper types:
|
||||
|
||||
```rust
|
||||
struct UserId(u64);
|
||||
struct OrderId(u64);
|
||||
|
||||
fn get_order(user: UserId, order: OrderId) -> anyhow::Result<Order> {
|
||||
// Can't accidentally swap user and order IDs at call sites
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## Enum State Machines
|
||||
|
||||
Model states as enums — make illegal states unrepresentable:
|
||||
|
||||
```rust
|
||||
enum ConnectionState {
|
||||
Disconnected,
|
||||
Connecting { attempt: u32 },
|
||||
Connected { session_id: String },
|
||||
Failed { reason: String, retries: u32 },
|
||||
}
|
||||
|
||||
fn handle(state: &ConnectionState) {
|
||||
match state {
|
||||
ConnectionState::Disconnected => connect(),
|
||||
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
|
||||
ConnectionState::Connecting { .. } => wait(),
|
||||
ConnectionState::Connected { session_id } => use_session(session_id),
|
||||
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
|
||||
ConnectionState::Failed { reason, .. } => log_failure(reason),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Always match exhaustively — no wildcard `_` for business-critical enums.
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
Use for structs with many optional parameters:
|
||||
|
||||
```rust
|
||||
pub struct ServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
|
||||
ServerConfigBuilder {
|
||||
host: host.into(),
|
||||
port,
|
||||
max_connections: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerConfigBuilder {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfigBuilder {
|
||||
pub fn max_connections(mut self, n: usize) -> Self {
|
||||
self.max_connections = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ServerConfig {
|
||||
ServerConfig {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
max_connections: self.max_connections,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sealed Traits for Extensibility Control
|
||||
|
||||
Use a private module to seal a trait, preventing external implementations:
|
||||
|
||||
```rust
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub trait Format: private::Sealed {
|
||||
fn encode(&self, data: &[u8]) -> Vec<u8>;
|
||||
}
|
||||
|
||||
pub struct Json;
|
||||
impl private::Sealed for Json {}
|
||||
impl Format for Json {
|
||||
fn encode(&self, data: &[u8]) -> Vec<u8> { todo!() }
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Envelope
|
||||
|
||||
Consistent API responses using a generic enum:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum ApiResponse<T: serde::Serialize> {
|
||||
#[serde(rename = "ok")]
|
||||
Ok { data: T },
|
||||
#[serde(rename = "error")]
|
||||
Error { message: String },
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `rust-patterns` for comprehensive patterns including ownership, traits, generics, concurrency, and async.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Rust-specific content.
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- Never hardcode API keys, tokens, or credentials in source code
|
||||
- Use environment variables: `std::env::var("API_KEY")`
|
||||
- Fail fast if required secrets are missing at startup
|
||||
- Keep `.env` files in `.gitignore`
|
||||
|
||||
```rust
|
||||
// BAD
|
||||
const API_KEY: &str = "sk-abc123...";
|
||||
|
||||
// GOOD — environment variable with early validation
|
||||
fn load_api_key() -> anyhow::Result<String> {
|
||||
std::env::var("PAYMENT_API_KEY")
|
||||
.context("PAYMENT_API_KEY must be set")
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- Always use parameterized queries — never format user input into SQL strings
|
||||
- Use query builder or ORM (sqlx, diesel, sea-orm) with bind parameters
|
||||
|
||||
```rust
|
||||
// BAD — SQL injection via format string
|
||||
let query = format!("SELECT * FROM users WHERE name = '{name}'");
|
||||
sqlx::query(&query).fetch_one(&pool).await?;
|
||||
|
||||
// GOOD — parameterized query with sqlx
|
||||
// Placeholder syntax varies by backend: Postgres: $1 | MySQL: ? | SQLite: $1
|
||||
sqlx::query("SELECT * FROM users WHERE name = $1")
|
||||
.bind(&name)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Validate all user input at system boundaries before processing
|
||||
- Use the type system to enforce invariants (newtype pattern)
|
||||
- Parse, don't validate — convert unstructured data to typed structs at the boundary
|
||||
- Reject invalid input with clear error messages
|
||||
|
||||
```rust
|
||||
// Parse, don't validate — invalid states are unrepresentable
|
||||
pub struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn parse(input: &str) -> Result<Self, ValidationError> {
|
||||
let trimmed = input.trim();
|
||||
let at_pos = trimmed.find('@')
|
||||
.filter(|&p| p > 0 && p < trimmed.len() - 1)
|
||||
.ok_or_else(|| ValidationError::InvalidEmail(input.to_string()))?;
|
||||
let domain = &trimmed[at_pos + 1..];
|
||||
if trimmed.len() > 254 || !domain.contains('.') {
|
||||
return Err(ValidationError::InvalidEmail(input.to_string()));
|
||||
}
|
||||
// For production use, prefer a validated email crate (e.g., `email_address`)
|
||||
Ok(Self(trimmed.to_string()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Unsafe Code
|
||||
|
||||
- Minimize `unsafe` blocks — prefer safe abstractions
|
||||
- Every `unsafe` block must have a `// SAFETY:` comment explaining the invariant
|
||||
- Never use `unsafe` to bypass the borrow checker for convenience
|
||||
- Audit all `unsafe` code during review — it is a red flag without justification
|
||||
- Prefer `safe` FFI wrappers around C libraries
|
||||
|
||||
```rust
|
||||
// GOOD — safety comment documents ALL required invariants
|
||||
let widget: &Widget = {
|
||||
// SAFETY: `ptr` is non-null, aligned, points to an initialized Widget,
|
||||
// and no mutable references or mutations exist for its lifetime.
|
||||
unsafe { &*ptr }
|
||||
};
|
||||
|
||||
// BAD — no safety justification
|
||||
unsafe { &*ptr }
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- Run `cargo audit` to scan for known CVEs in dependencies
|
||||
- Run `cargo deny check` for license and advisory compliance
|
||||
- Use `cargo tree` to audit transitive dependencies
|
||||
- Keep dependencies updated — set up Dependabot or Renovate
|
||||
- Minimize dependency count — evaluate before adding new crates
|
||||
|
||||
```bash
|
||||
# Security audit
|
||||
cargo audit
|
||||
|
||||
# Deny advisories, duplicate versions, and restricted licenses
|
||||
cargo deny check
|
||||
|
||||
# Inspect dependency tree
|
||||
cargo tree
|
||||
cargo tree -d # Show duplicates only
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
|
||||
- Never expose internal paths, stack traces, or database errors in API responses
|
||||
- Log detailed errors server-side; return generic messages to clients
|
||||
- Use `tracing` or `log` for structured server-side logging
|
||||
|
||||
```rust
|
||||
// Map errors to appropriate status codes and generic messages
|
||||
// (Example uses axum; adapt the response type to your framework)
|
||||
match order_service.find_by_id(id) {
|
||||
Ok(order) => Ok((StatusCode::OK, Json(order))),
|
||||
Err(ServiceError::NotFound(_)) => {
|
||||
tracing::info!(order_id = id, "order not found");
|
||||
Err((StatusCode::NOT_FOUND, "Resource not found"))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(order_id = id, error = %e, "unexpected error");
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `rust-patterns` for unsafe code guidelines and ownership patterns.
|
||||
See skill: `security-review` for general security checklists.
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Rust-specific content.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- **`#[test]`** with `#[cfg(test)]` modules for unit tests
|
||||
- **rstest** for parameterized tests and fixtures
|
||||
- **proptest** for property-based testing
|
||||
- **mockall** for trait-based mocking
|
||||
- **`#[tokio::test]`** for async tests
|
||||
|
||||
## Test Organization
|
||||
|
||||
```text
|
||||
my_crate/
|
||||
├── src/
|
||||
│ ├── lib.rs # Unit tests in #[cfg(test)] modules
|
||||
│ ├── auth/
|
||||
│ │ └── mod.rs # #[cfg(test)] mod tests { ... }
|
||||
│ └── orders/
|
||||
│ └── service.rs # #[cfg(test)] mod tests { ... }
|
||||
├── tests/ # Integration tests (each file = separate binary)
|
||||
│ ├── api_test.rs
|
||||
│ ├── db_test.rs
|
||||
│ └── common/ # Shared test utilities
|
||||
│ └── mod.rs
|
||||
└── benches/ # Criterion benchmarks
|
||||
└── benchmark.rs
|
||||
```
|
||||
|
||||
Unit tests go inside `#[cfg(test)]` modules in the same file. Integration tests go in `tests/`.
|
||||
|
||||
## Unit Test Pattern
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_valid_email() {
|
||||
let user = User::new("Alice", "alice@example.com").unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_email() {
|
||||
let result = User::new("Bob", "not-an-email");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("invalid email"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parameterized Tests
|
||||
|
||||
```rust
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case("hello", 5)]
|
||||
#[case("", 0)]
|
||||
#[case("rust", 4)]
|
||||
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
|
||||
assert_eq!(input.len(), expected);
|
||||
}
|
||||
```
|
||||
|
||||
## Async Tests
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn fetches_data_successfully() {
|
||||
let client = TestClient::new().await;
|
||||
let result = client.get("/data").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
## Mocking with mockall
|
||||
|
||||
Define traits in production code; generate mocks in test modules:
|
||||
|
||||
```rust
|
||||
// Production trait — pub so integration tests can import it
|
||||
pub trait UserRepository {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mockall::predicate::eq;
|
||||
|
||||
mockall::mock! {
|
||||
pub Repo {}
|
||||
impl UserRepository for Repo {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_user_when_found() {
|
||||
let mut mock = MockRepo::new();
|
||||
mock.expect_find_by_id()
|
||||
.with(eq(42))
|
||||
.times(1)
|
||||
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
let user = service.get_user(42).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Naming
|
||||
|
||||
Use descriptive names that explain the scenario:
|
||||
- `creates_user_with_valid_email()`
|
||||
- `rejects_order_when_insufficient_stock()`
|
||||
- `returns_none_when_not_found()`
|
||||
|
||||
## Coverage
|
||||
|
||||
- Target 80%+ line coverage
|
||||
- Use **cargo-llvm-cov** for coverage reporting
|
||||
- Focus on business logic — exclude generated code and FFI bindings
|
||||
|
||||
```bash
|
||||
cargo llvm-cov # Summary
|
||||
cargo llvm-cov --html # HTML report
|
||||
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
cargo test # Run all tests
|
||||
cargo test -- --nocapture # Show println output
|
||||
cargo test test_name # Run tests matching pattern
|
||||
cargo test --lib # Unit tests only
|
||||
cargo test --test api_test # Specific integration test (tests/api_test.rs)
|
||||
cargo test --doc # Doc tests only
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `rust-testing` for comprehensive testing patterns including property-based testing, fixtures, and benchmarking with Criterion.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Swift specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **SwiftFormat** for auto-formatting, **SwiftLint** for style enforcement
|
||||
- `swift-format` is bundled with Xcode 16+ as an alternative
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer `let` over `var` — define everything as `let` and only change to `var` if the compiler requires it
|
||||
- Use `struct` with value semantics by default; use `class` only when identity or reference semantics are needed
|
||||
|
||||
## Naming
|
||||
|
||||
Follow [Apple API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/):
|
||||
|
||||
- Clarity at the point of use — omit needless words
|
||||
- Name methods and properties for their roles, not their types
|
||||
- Use `static let` for constants over global constants
|
||||
|
||||
## Error Handling
|
||||
|
||||
Use typed throws (Swift 6+) and pattern matching:
|
||||
|
||||
```swift
|
||||
func load(id: String) throws(LoadError) -> Item {
|
||||
guard let data = try? read(from: path) else {
|
||||
throw .fileNotFound(id)
|
||||
}
|
||||
return try decode(data)
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrency
|
||||
|
||||
Enable Swift 6 strict concurrency checking. Prefer:
|
||||
|
||||
- `Sendable` value types for data crossing isolation boundaries
|
||||
- Actors for shared mutable state
|
||||
- Structured concurrency (`async let`, `TaskGroup`) over unstructured `Task {}`
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with Swift specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **SwiftFormat**: Auto-format `.swift` files after edit
|
||||
- **SwiftLint**: Run lint checks after editing `.swift` files
|
||||
- **swift build**: Type-check modified packages after edit
|
||||
|
||||
## Warning
|
||||
|
||||
Flag `print()` statements — use `os.Logger` or structured logging instead for production code.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Swift specific content.
|
||||
|
||||
## Protocol-Oriented Design
|
||||
|
||||
Define small, focused protocols. Use protocol extensions for shared defaults:
|
||||
|
||||
```swift
|
||||
protocol Repository: Sendable {
|
||||
associatedtype Item: Identifiable & Sendable
|
||||
func find(by id: Item.ID) async throws -> Item?
|
||||
func save(_ item: Item) async throws
|
||||
}
|
||||
```
|
||||
|
||||
## Value Types
|
||||
|
||||
- Use structs for data transfer objects and models
|
||||
- Use enums with associated values to model distinct states:
|
||||
|
||||
```swift
|
||||
enum LoadState<T: Sendable>: Sendable {
|
||||
case idle
|
||||
case loading
|
||||
case loaded(T)
|
||||
case failed(Error)
|
||||
}
|
||||
```
|
||||
|
||||
## Actor Pattern
|
||||
|
||||
Use actors for shared mutable state instead of locks or dispatch queues:
|
||||
|
||||
```swift
|
||||
actor Cache<Key: Hashable & Sendable, Value: Sendable> {
|
||||
private var storage: [Key: Value] = [:]
|
||||
|
||||
func get(_ key: Key) -> Value? { storage[key] }
|
||||
func set(_ key: Key, value: Value) { storage[key] = value }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Inject protocols with default parameters — production uses defaults, tests inject mocks:
|
||||
|
||||
```swift
|
||||
struct UserService {
|
||||
private let repository: any UserRepository
|
||||
|
||||
init(repository: any UserRepository = DefaultUserRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `swift-actor-persistence` for actor-based persistence patterns.
|
||||
See skill: `swift-protocol-di-testing` for protocol-based DI and testing.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Swift specific content.
|
||||
|
||||
## Secret Management
|
||||
|
||||
- Use **Keychain Services** for sensitive data (tokens, passwords, keys) — never `UserDefaults`
|
||||
- Use environment variables or `.xcconfig` files for build-time secrets
|
||||
- Never hardcode secrets in source — decompilation tools extract them trivially
|
||||
|
||||
```swift
|
||||
let apiKey = ProcessInfo.processInfo.environment["API_KEY"]
|
||||
guard let apiKey, !apiKey.isEmpty else {
|
||||
fatalError("API_KEY not configured")
|
||||
}
|
||||
```
|
||||
|
||||
## Transport Security
|
||||
|
||||
- App Transport Security (ATS) is enforced by default — do not disable it
|
||||
- Use certificate pinning for critical endpoints
|
||||
- Validate all server certificates
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Sanitize all user input before display to prevent injection
|
||||
- Use `URL(string:)` with validation rather than force-unwrapping
|
||||
- Validate data from external sources (APIs, deep links, pasteboard) before processing
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Swift specific content.
|
||||
|
||||
## Framework
|
||||
|
||||
Use **Swift Testing** (`import Testing`) for new tests. Use `@Test` and `#expect`:
|
||||
|
||||
```swift
|
||||
@Test("User creation validates email")
|
||||
func userCreationValidatesEmail() throws {
|
||||
#expect(throws: ValidationError.invalidEmail) {
|
||||
try User(email: "not-an-email")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Isolation
|
||||
|
||||
Each test gets a fresh instance — set up in `init`, tear down in `deinit`. No shared mutable state between tests.
|
||||
|
||||
## Parameterized Tests
|
||||
|
||||
```swift
|
||||
@Test("Validates formats", arguments: ["json", "xml", "csv"])
|
||||
func validatesFormat(format: String) throws {
|
||||
let parser = try Parser(format: format)
|
||||
#expect(parser.isValid)
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
swift test --enable-code-coverage
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See skill: `swift-protocol-di-testing` for protocol-based dependency injection and mock patterns with Swift Testing.
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with TypeScript/JavaScript specific content.
|
||||
|
||||
## Types and Interfaces
|
||||
|
||||
Use types to make public APIs, shared models, and component props explicit, readable, and reusable.
|
||||
|
||||
### Public APIs
|
||||
|
||||
- Add parameter and return types to exported functions, shared utilities, and public class methods
|
||||
- Let TypeScript infer obvious local variable types
|
||||
- Extract repeated inline object shapes into named types or interfaces
|
||||
|
||||
```typescript
|
||||
// WRONG: Exported function without explicit types
|
||||
export function formatUser(user) {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
|
||||
// CORRECT: Explicit types on public APIs
|
||||
interface User {
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
export function formatUser(user: User): string {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
```
|
||||
|
||||
### Interfaces vs. Type Aliases
|
||||
|
||||
- Use `interface` for object shapes that may be extended or implemented
|
||||
- Use `type` for unions, intersections, tuples, mapped types, and utility types
|
||||
- Prefer string literal unions over `enum` unless an `enum` is required for interoperability
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type UserRole = 'admin' | 'member'
|
||||
type UserWithRole = User & {
|
||||
role: UserRole
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid `any`
|
||||
|
||||
- Avoid `any` in application code
|
||||
- Use `unknown` for external or untrusted input, then narrow it safely
|
||||
- Use generics when a value's type depends on the caller
|
||||
|
||||
```typescript
|
||||
// WRONG: any removes type safety
|
||||
function getErrorMessage(error: any) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// CORRECT: unknown forces safe narrowing
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return 'Unexpected error'
|
||||
}
|
||||
```
|
||||
|
||||
### React Props
|
||||
|
||||
- Define component props with a named `interface` or `type`
|
||||
- Type callback props explicitly
|
||||
- Do not use `React.FC` unless there is a specific reason to do so
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface UserCardProps {
|
||||
user: User
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
function UserCard({ user, onSelect }: UserCardProps) {
|
||||
return <button onClick={() => onSelect(user.id)}>{user.email}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript Files
|
||||
|
||||
- In `.js` and `.jsx` files, use JSDoc when types improve clarity and a TypeScript migration is not practical
|
||||
- Keep JSDoc aligned with runtime behavior
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @param {{ firstName: string, lastName: string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatUser(user) {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
```
|
||||
|
||||
## Immutability
|
||||
|
||||
Use spread operator for immutable updates:
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// WRONG: Mutation
|
||||
function updateUser(user: User, name: string): User {
|
||||
user.name = name // MUTATION!
|
||||
return user
|
||||
}
|
||||
|
||||
// CORRECT: Immutability
|
||||
function updateUser(user: Readonly<User>, name: string): User {
|
||||
return {
|
||||
...user,
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Use async/await with try-catch and narrow unknown errors safely:
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
declare function riskyOperation(userId: string): Promise<User>
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return 'Unexpected error'
|
||||
}
|
||||
|
||||
const logger = {
|
||||
error: (message: string, error: unknown) => {
|
||||
// Replace with your production logger (for example, pino or winston).
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUser(userId: string): Promise<User> {
|
||||
try {
|
||||
const result = await riskyOperation(userId)
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
logger.error('Operation failed', error)
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
Use Zod for schema-based validation and infer types from the schema:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const userSchema = z.object({
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
type UserInput = z.infer<typeof userSchema>
|
||||
|
||||
const validated: UserInput = userSchema.parse(input)
|
||||
```
|
||||
|
||||
## Console.log
|
||||
|
||||
- No `console.log` statements in production code
|
||||
- Use proper logging libraries instead
|
||||
- See hooks for automatic detection
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript Hooks
|
||||
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with TypeScript/JavaScript specific content.
|
||||
|
||||
## PostToolUse Hooks
|
||||
|
||||
Configure in `~/.claude/settings.json`:
|
||||
|
||||
- **Prettier**: Auto-format JS/TS files after edit
|
||||
- **TypeScript check**: Run `tsc` after editing `.ts`/`.tsx` files
|
||||
- **console.log warning**: Warn about `console.log` in edited files
|
||||
|
||||
## Stop Hooks
|
||||
|
||||
- **console.log audit**: Check all modified files for `console.log` before session ends
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with TypeScript/JavaScript specific content.
|
||||
|
||||
## API Response Format
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
meta?: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks Pattern
|
||||
|
||||
```typescript
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```typescript
|
||||
interface Repository<T> {
|
||||
findAll(filters?: Filters): Promise<T[]>
|
||||
findById(id: string): Promise<T | null>
|
||||
create(data: CreateDto): Promise<T>
|
||||
update(id: string, data: UpdateDto): Promise<T>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with TypeScript/JavaScript specific content.
|
||||
|
||||
## Secret Management
|
||||
|
||||
```typescript
|
||||
// NEVER: Hardcoded secrets
|
||||
const apiKey = "sk-proj-xxxxx"
|
||||
|
||||
// ALWAYS: Environment variables
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
## Agent Support
|
||||
|
||||
- Use **security-reviewer** skill for comprehensive security audits
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with TypeScript/JavaScript specific content.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
Use **Playwright** as the E2E testing framework for critical user flows.
|
||||
|
||||
## Agent Support
|
||||
|
||||
- **e2e-runner** - Playwright E2E testing specialist
|
||||
Reference in New Issue
Block a user