Static Code Analysis
Overview
Use automated tools to analyze code without executing it, catching bugs, security issues, and style violations early.
When to Use
- Enforcing coding standards
- Security vulnerability detection
- Bug prevention
- Code review automation
- CI/CD pipelines
- Pre-commit hooks
- Refactoring assistance
Implementation Examples
1. ESLint Configuration
// .eslintrc.js
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:security/recommended",
],
plugins: ["@typescript-eslint", "security", "import"],
rules: {
"no-console": ["warn", { allow: ["error", "warn"] }],
"no-unused-vars": "error",
"prefer-const": "error",
eqeqeq: ["error", "always"],
"no-eval": "error",
"security/detect-object-injection": "warn",
"security/detect-non-literal-regexp": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "error",
"import/order": [
"error",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
"newlines-between": "always",
},
],
},
};
2. Python Linting (pylint + mypy)
# .pylintrc
[MASTER]
ignore=venv,.git,__pycache__
jobs=4
[MESSAGES CONTROL]
disable=
missing-docstring,
too-few-public-methods
[FORMAT]
max-line-length=100
max-module-lines=1000
[DESIGN]
max-args=5
max-locals=15
max-returns=6
max-branches=12
max-statements=50
# mypy.ini
[mypy]
python_version = 3.10
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_calls = True
warn_redundant_casts = True
warn_unused_ignores = True
strict_equality = True
3. Pre-commit Hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-merge-conflict
- id: detect-private-key
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.50.0
hooks:
- id: eslint
files: \.[jt]sx?$
types: [file]
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/PyCQA/pylint
rev: v3.0.0
hooks:
- id: pylint
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
hooks:
- id: mypy
additional_dependencies: [types-requests]
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.58.0
hooks:
- id: trufflehog
entry: trufflehog filesystem --directory .
4. SonarQube Integration
# sonar-project.properties
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0
sonar.sources=src
sonar.tests=tests
sonar.exclusions=**/node_modules/**,**/*.test.ts
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.qualitygate.wait=true
# Quality gates
sonar.coverage.exclusions=**/*.test.ts
# .github/workflows/sonar.yml
name: SonarQube Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: Quality Gate Check
uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
5. Custom AST Analysis
import * as ts from "typescript";
import * as fs from "fs";
interface Issue {
file: string;
line: number;
column: number;
message: string;
severity: "error" | "warning" | "info";
rule: string;
}
class CustomLinter {
private issues: Issue[] = [];
lintFile(filePath: string): Issue[] {
this.issues = [];
const sourceCode = fs.readFileSync(filePath, "utf-8");
const sourceFile = ts.createSourceFile(
filePath,
sourceCode,
ts.ScriptTarget.Latest,
true,
);
this.visit(sourceFile, filePath);
return this.issues;
}
private visit(node: ts.Node, filePath: string): void {
// Check for console.log
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.expression.getText() === "console" &&
node.expression.name.getText() === "log"
) {
const { line, character } = ts.getLineAndCharacterOfPosition(
node.getSourceFile(),
node.getStart(),
);
this.issues.push({
file: filePath,
line: line + 1,
column: character + 1,
message: "Unexpected console.log statement",
severity: "warning",
rule: "no-console",
});
}
// Check for any type
if (ts.isTypeReferenceNode(node) && node.typeName.getText() === "any") {
const { line, character } = ts.getLineAndCharacterOfPosition(
node.getSourceFile(),
node.getStart(),
);
this.issues.push({
file: filePath,
line: line + 1,
column: character + 1,
message: "Avoid using any type",
severity: "warning",
rule: "no-any",
});
}
// Check for long functions
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
const body = node.body;
if (body && body.getFullText().split("\n").length > 50) {
const { line, character } = ts.getLineAndCharacterOfPosition(
node.getSourceFile(),
node.getStart(),
);
this.issues.push({
file: filePath,
line: line + 1,
column: character + 1,
message: "Function is too long (>50 lines)",
severity: "warning",
rule: "max-lines-per-function",
});
}
}
ts.forEachChild(node, (child) => this.visit(child, filePath));
}
formatIssues(issues: Issue[]): string {
if (issues.length === 0) {
return "No issues found.";
}
return issues
.map(
(issue) =>
`${issue.file}:${issue.line}:${issue.column} - ${issue.severity}: ${issue.message} (${issue.rule})`,
)
.join("\n");
}
}
// Usage
const linter = new CustomLinter();
const issues = linter.lintFile("./src/example.ts");
console.log(linter.formatIssues(issues));
6. Security Scanning
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
interface SecurityIssue {
severity: "critical" | "high" | "medium" | "low";
title: string;
description: string;
file?: string;
line?: number;
remediation?: string;
}
class SecurityScanner {
async scanDependencies(): Promise<SecurityIssue[]> {
try {
const { stdout } = await execAsync("npm audit --json");
const auditResult = JSON.parse(stdout);
const issues: SecurityIssue[] = [];
for (const [name, advisory] of Object.entries(
auditResult.vulnerabilities || {},
)) {
const vuln = advisory as any;
issues.push({
severity: vuln.severity,
title: vuln.via[0]?.title || name,
description: vuln.via[0]?.url || "",
remediation: `Update ${name} to ${vuln.fixAvailable || "latest"}`,
});
}
return issues;
} catch (error) {
console.error("Dependency scan failed:", error);
return [];
}
}
async scanSecrets(directory: string): Promise<SecurityIssue[]> {
const issues: SecurityIssue[] = [];
// Simple regex-based secret detection
const patterns = [
{
name: "API Key",
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9]{32,})['"]/,
},
{ name: "AWS Key", pattern: /(AKIA[0-9A-Z]{16})/ },
{
name: "Private Key",
pattern: /-----BEGIN (RSA |EC )?PRIVATE KEY-----/,
},
{
name: "Password",
pattern: /password['"]?\s*[:=]\s*['"]((?!<%= ).{8,})['"]/,
},
];
// Scan files
const files = this.getFiles(directory);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
for (const { name, pattern } of patterns) {
if (pattern.test(lines[i])) {
issues.push({
severity: "critical",
title: `Potential ${name} detected`,
description: `Found in ${file}:${i + 1}`,
file,
line: i + 1,
remediation: "Remove secret and use environment variables",
});
}
}
}
}
return issues;
}
private getFiles(dir: string): string[] {
// Implementation to recursively get files
return [];
}
generateReport(issues: SecurityIssue[]): string {
let report = "# Security Scan Report\n\n";
const grouped = issues.reduce(
(acc, issue) => {
acc[issue.severity] = acc[issue.severity] || [];
acc[issue.severity].push(issue);
return acc;
},
{} as Record<string, SecurityIssue[]>,
);
for (const [severity, items] of Object.entries(grouped)) {
report += `## ${severity.toUpperCase()} (${items.length})\n\n`;
for (const issue of items) {
report += `### ${issue.title}\n`;
report += `${issue.description}\n`;
if (issue.remediation) {
report += `**Remediation:** ${issue.remediation}\n`;
}
report += "\n";
}
}
return report;
}
}
// Usage
const scanner = new SecurityScanner();
const depIssues = await scanner.scanDependencies();
const secretIssues = await scanner.scanSecrets("./src");
const allIssues = [...depIssues, ...secretIssues];
console.log(scanner.generateReport(allIssues));
Best Practices
✅ DO
- Run linters in CI/CD
- Use pre-commit hooks
- Configure IDE integration
- Fix issues incrementally
- Document custom rules
- Share configuration across team
- Automate security scanning
❌ DON'T
- Ignore all warnings
- Skip linter setup
- Commit lint violations
- Use overly strict rules initially
- Skip security scans
- Disable rules without reason
Tools
- JavaScript/TypeScript: ESLint, TSLint, Prettier
- Python: Pylint, Flake8, Black, Bandit
- Java: PMD, Checkstyle, SpotBugs
- Security: Snyk, Semgrep, Bandit, TruffleHog
- Multi-language: SonarQube, CodeQL