Blog
Static code analysis vs AI code review: what each catches
Where rule-based checks stop and AI review on the pull request picks up, with real tool output and open-source analyzers for common languages.
Alex Mercer
Static code analysis is the automated checking of source code without running it. A tool parses your code into a structure it can inspect, then applies rules that flag bugs, security weaknesses, type errors and style problems.
In AI code review, a large language model reads a pull request together with the code around it and comments on problems that no rule describes, such as a change in one file that breaks an assumption in another. The two find different classes of bugs, and each one is cheapest at a different point in your workflow.
What static code analysis is
The static analysis chapter of Software Engineering at Google defines it as "programs analyzing source code to find potential issues such as bugs, antipatterns, and other issues that can be diagnosed without executing the program." Static means the program never runs. Tests, fuzzers and sanitizers run it, which makes them dynamic analysis.
In practice the term covers tools you probably run already:
Linters flag suspicious patterns and style problems: ESLint for JavaScript, Ruff and Pylint for Python, RuboCop for Ruby.
Type checkers make sure values are used the way their types allow: mypy for Python, the TypeScript compiler in strict mode.
Compilers reject some mistakes outright. Go's compiler treats an unused variable or import as an error and stops the build.
Security linters match known vulnerability shapes, such as SQL assembled from strings.
How static analysis works
Most analyzers start where a compiler starts. A lexer turns source text into tokens, and a parser builds an abstract syntax tree (AST) from them: a tree of the program's structure in which every operator, call and name is a node. Python exposes its own parser through the standard ast module, so you can print the kind of tree a Python linter works on:
A rule is a check that runs on nodes of that tree. Ruff's rule E711 looks for this shape, an equality comparison with None on one side, and asks you to write customer is None. ESLint works the same way for JavaScript: a rule returns functions that ESLint calls to visit nodes as it walks the tree (custom rules docs). PMD parses Java into an AST and runs rules written in Java or as XPath queries against it. Bandit builds an AST for each Python file and runs its security plugins on the nodes.
Shapes only go so far. Deeper analyzers also track what values can be:
A type checker uses your annotations to work out what each expression can hold. mypy knows a
str | Nonevalue may beNone, so it rejects a call to.split()on it until you check.Control flow analysis turns a function into a graph of basic blocks joined by jumps, and data flow analysis follows values along the paths through that graph.
Taint analysis, a data flow technique built for security, marks user-controlled input and flags it when it reaches a sink, such as a query, without being sanitized (OWASP).
Deeper analysis is slower. The Clang Static Analyzer documentation says static analysis "can be much slower than compilation" and that some of the algorithms it needs take exponential time in the worst case, so the analyzer bounds how much checking it does.
What static analysis catches reliably
Take a small file like this one:
Run Ruff with its pycodestyle, Pyflakes, bugbear and bandit rule sets (E, F, B and S), then mypy:
E711 is a PEP 8 style rule. The other three findings are bugs. add_tag shares one list across every call, because Python evaluates a default argument once, when the function is defined. email_domain crashes for a customer with no email. The f-string puts customer_id straight into SQL, an injection hole if that value ever comes from a user.
Both tools finished within about a second on this file, and they report the same findings on every run as long as the code and config stay the same. Because the output is deterministic, you can fail a commit or a CI job on it without anyone reading it first.
Rules also cover a lot of ground: go vet checks that Printf arguments match the format string, SpotBugs checks Java for more than 400 bug patterns, Clippy ships over 800 lints, and style checkers like Checkstyle and RuboCop settle naming and formatting before a reviewer sees the code. OWASP rates security scanners as great "for things that such tools can automatically find with high confidence, such as buffer overflows, SQL Injection Flaws."
What static analysis misses
Rules see what is written in the code: syntax, types and data flow. They don't know why a change exists, and they only know what the rest of the system assumes when a type or a rule spells it out.
Logic that spans files
This pull request moves plan prices from dollars to cents:
Ruff with the same rule sets passes both files, and so does mypy --strict. plan_price returns an int before and after the change, so as far as the tools can tell nothing is wrong. The invoice for one starter seat now reads starter x1: $2,900.00.
Intent and business rules
No rule knows what the ticket asked for, so code that cleanly does the wrong thing passes. The same goes for business rules like "refunds above a set amount need a second approval": unless someone writes a custom check, that rule lives in a spec or in someone's head.
Security that depends on context
OWASP lists "authentication problems, access control issues, insecure use of cryptography" among the vulnerabilities that are "very difficult to find automatically," and notes that scanners "frequently can't find configuration issues, since they are not represented in the code." To a pattern matcher, a route that forgets the permission check looks fine, even when every other route in the file runs it.
False positives
Rules also fire on code that is fine. go vet's documentation says it "uses heuristics that do not guarantee all reports are genuine problems," and the Clang Static Analyzer's says it "can falsely flag bugs in a program where the code behaves correctly." The S608 finding above matches the shape of a SQL string, so it fires whether or not customer_id can come from a user. OWASP lists "high numbers of false positives" as a weakness of security scanners.
Noise costs more than the minutes spent dismissing it. The Google chapter reports that "developers ignore compiler warnings," and it calls an issue an "effective false positive" if developers took no positive action after seeing it. Google's criteria for checks shown in code review include fewer than 10% effective false positives.
What AI code review adds
An AI reviewer starts from the pull request. It gathers the diff, the full files it touches, the code that calls the changed code and any rule files in the repository, often with the linked ticket. Then it comments inline on the lines it thinks are wrong, usually with a suggested fix. Because it reads the caller as well as the diff, the cents change is the kind of bug it is built to catch. A review comment might say:
plan_pricenow returns cents, butinvoice_lineinbilling/invoice.pystill formats the result as dollars, so one starter seat prints as $2,900.00. Divide by 100 there, or give cents their own type so the type checker catches the next caller.
The same context covers bugs that look fine line by line: a renamed field that a background job in another folder still reads, a catch block that logs a failed payment and returns success, a route that skips the membership check its neighbors run, a test that mocks the function it is meant to check. An AI reviewer also applies rules you write in plain English, such as "store money in integer cents."
Code written by AI coding agents gets review findings too. Bug Bench is cubic's public benchmark. It counts a cubic review finding on AI-written code as a bug when a developer upvoted it or an AI coding agent addressed it, and ranks models by relative bug rate: counted bugs per AI-attributed line, compared with the rate across all AI-attributed lines. Relative bug rates for the seven ranked models run from 0.65 to 1.50, where 1.00 is average, so every ranked model's code produced counted bugs. The benchmark is observational: teams choose their models, so a rate reflects the code each model was used for as well as the model.
Where AI code review is weaker
AI review is not deterministic. Run it twice on the same pull request and the results can vary, which makes it a poor merge blocker. Models also write plausible comments that are wrong, and a team that learns to skim a noisy reviewer misses the comment that mattered.
Most AI reviewers read code without running it, so a clean review doesn't prove the code works. Tests still own that question. Each review also takes minutes and model inference, where a linter takes seconds of CPU.
The reviewer only knows what it can read. A table's size in production, or a partner still calling an old endpoint, is invisible unless someone wrote it down where the reviewer can see it. Whether the change should exist at all, and which design trade-off to accept, stay with the people on your team.
Static code analysis vs AI code review compared
Static code analysis | AI code review | |
|---|---|---|
What it checks | Code against fixed rules: syntax, types, known bug and security patterns, style | The change in context: the diff, related files, your written rules, often the ticket |
How it decides | Rules over syntax trees, types and data flow | A language model reading the code |
Determinism | Same code and config, same findings | Findings and wording can vary between runs |
False positives | Common in heuristic and security rules; tuned by disabling rules or suppressing lines | Plausible but wrong comments; good tools check findings against the code and learn from replies |
Setup | A tool per language and a config file, wired into the editor, pre-commit and CI | An app on your Git host, plus rules in plain English |
Where it runs | Editor, pre-commit hook, CI | The pull request; many tools also run in a terminal before you push |
Speed | Seconds for linters; deep analyzers take longer | Minutes |
Cost per run | CPU time; the common tools are open source | Model inference on every review, paid per seat or by usage |
Safe to block merges | Yes, for rules with few false positives | Better as comments that a person weighs |
Static code analysis tools
Open-source tools by language
All of these static analysis tools are open source. For each language you use, start with a linter, add a type checker if the language has optional types, and add a security linter where one exists.
Language | Tool | What it checks |
|---|---|---|
JavaScript, TypeScript | Patterns in JavaScript code, to make it consistent and avoid bugs | |
TypeScript | The full family of strict type checks | |
Python | Lint rules (over 900 built in) and formatting, written in Rust | |
Python | Errors, coding standards and code smells, inferring values when code isn't fully typed | |
Python | Types, from your type hints | |
Python | Common security issues, found by plugins that run on each file's AST | |
Go | Suspicious constructs, such as Printf calls whose arguments don't match the format string | |
Go | Bugs, performance issues, simplifications and style, in 150+ checks | |
Rust | Common mistakes, in over 800 lints grouped into categories | |
Java | Common mistakes, checked inside the compiler as you build | |
Java | More than 400 bug patterns in compiled Java; a fork of the abandoned FindBugs | |
Java, Apex and 16 more | Flaws like unused variables and empty catch blocks, with 400+ rules and a copy-paste detector | |
Java | Coding standards: naming, imports, formatting, Javadoc and class design | |
Ruby | The community Ruby Style Guide, as a linter and formatter | |
C, C++, Objective-C | Deeper bugs of the kind usually found by testing, at a slower speed than compiling | |
C++ | Style violations, interface misuse and bugs that static analysis can deduce |
Multi-language tools: SonarQube, Semgrep, CodeQL and Snyk Code
Most tools above cover one language. These four scan many languages from one setup, and each has a free option.
Tool | What it does | Free option |
|---|---|---|
SonarQube | Bugs, vulnerabilities, security hotspots, code smells and architecture issues in 40+ languages. It runs as SonarQube Cloud (SaaS) or as self-managed SonarQube Server, with an IDE plugin | Community Build is free and self-managed, and analyzes only the main branch. The SonarQube Cloud Free plan also analyzes pull requests that target the main branch (feature comparison) |
Semgrep | Security issues, style violations and bugs, using YAML rules that combine pattern matching and data flow analysis. You can use rules from the Semgrep Registry or write your own | The Community Edition engine is open source under LGPL 2.1. Semgrep Code, its extension, is proprietary |
CodeQL | GitHub's analysis engine builds a database from your code, runs queries written in QL against it and shows the results as code scanning alerts. It supports C/C++, C#, Go, Java, Kotlin, JavaScript, TypeScript, Python, Ruby, Rust, Swift and GitHub Actions workflows | Free for public repositories on GitHub.com. Private repositories need GitHub Code Security, bought on a GitHub Team or Enterprise plan |
Snyk Code | Security vulnerabilities (SAST), showing the data flow of an issue from source to sink. It runs in the IDE, the CLI, the Snyk web UI and pull request checks | The Free plan includes 100 Snyk Code tests a month |
For how these compare with human review and AI review tools, see code review tools compared.
How to run static analysis and AI review together
Put each check where it runs cheapest and where its author can still act on it.
In the editor, run the linter and type checker as you type. Google's chapter notes that analyses in an IDE need to be fast, "typically less than 1 second and ideally less than 100 ms."
In a pre-commit hook, run the fast checks again. Git aborts the commit when the hook exits non-zero, but anyone can skip it with
--no-verify(githooks), so CI has to run the same checks.In CI, run all of it: linters, type checks, security rules and tests. Make those jobs required status checks so a pull request can't merge into a protected branch until they pass.
On the pull request, let AI review comment on what rules can't see, and have a person approve the intent and the design.
Block merges only on deterministic checks. In its most used languages, Google enables a compiler check only as an error that breaks the build, and only if it would "never stop the build for correct code"; checks that can't meet that bar are suppressed or shown in code review. AI review findings belong in that second group, as comments a person can accept or dismiss. For a rollout plan and the numbers that show whether it's working, read automated code review: what to automate, what to keep human.
Move repeat findings earlier. When AI review keeps catching the same mistake, turn it into a type or a lint rule so the editor catches it next time. Python's typing.NewType creates distinct types that a type checker keeps apart, so separate types for cents and dollars let mypy catch the billing mix-up:
Rules that no pattern can express go the other way, into plain language for the AI reviewer. The docs for cubic's custom review rules, for example, recommend focusing them "on checks that linters do not cover, such as business logic and team-specific conventions" (custom rules).
FAQ
Can AI do static code analysis?
Most AI reviewers analyze code without executing it, which fits the definition of static analysis. They don't apply a fixed rule set, though, and the same code can get different results, so they don't replace a linter. A useful middle ground is asking a coding agent to write a custom lint rule for a mistake your reviews keep finding: Pylint accepts plugins for your own checks and PMD accepts rules written as XPath queries, while Ruff runs only its built-in rules.
What is the difference between static and dynamic code analysis?
Static analysis reads the source, and dynamic analysis watches the program run. AddressSanitizer is a typical dynamic tool: it adds compiler instrumentation and a run-time library, catches out-of-bounds accesses and use-after-free bugs while your tests run, and slows the program down about 2x. Static analysis checks code whether or not a test reaches it, while dynamic analysis only sees the paths that ran but reports what really happened on them.
Is static code analysis the same as SAST?
SAST, static application security testing, is the security-focused part of static analysis. Bandit, the S rules in Ruff, Snyk Code and CodeQL's security queries are all SAST checks. A type checker or a style linter is static analysis too, but its rules are about correctness and consistency, so it isn't usually called SAST.
Is SonarQube a static code analysis tool?
Yes. SonarQube's documentation describes its free Community Build as an automated code review and static analysis tool that checks code against rules for maintainability, reliability and security. Showing quality gate status on pull requests, and blocking a merge when the gate fails, needs SonarQube Server or a SonarQube Cloud Team or Enterprise plan.
How do you reduce false positives from static analysis?
Turn off rules your team keeps dismissing, and silence single findings in the code where a rule is wrong for that line: # noqa: E711 for Ruff, // eslint-disable-next-line no-alert for ESLint, or # type: ignore[union-attr] for mypy, which silences only that error code. Report new warnings first. Google's analysis team focuses on newly introduced warnings and warnings on modified lines, where the developer seeing them has the most context.
