WEEK 11Monday

Static Analysis

Week 11 · Adding analyzers to Project Pulse, a codebase that never had them

WEEK 11Monday

One method from Project Pulse

public Result addActivityComment(...) {
    String comment = commentMap.get("comment");
    Activity updatedActivity = this.activityService.addActivityComment(activityId, comment);
    ActivityDto updatedActivityDto = this.activityToActivityDtoConverter.convert(updatedActivity);
    return new Result(true, StatusCode.SUCCESS, "Add comment successfully", null);
}
Ask the room

It compiled, and it sat on main from 2024 into 2026. What's wrong with it?

WEEK 11Monday

You've seen one already

Where you saw it The analyzer
Gray "variable is never used" IntelliJ, VS Code
Red squiggle in a .js file ESLint
"Object is possibly 'undefined'" TypeScript
GitHub email: vulnerable library Dependabot, CodeQL
"Quality gate failed" at your internship SonarQube
Key point

Static analysis: a program that reads your code without running it.

WEEK 11Monday

So why doesn't everyone run them?

  • They add no feature
  • An afternoon to set up
  • First run: hundreds of warnings, most on code that works

Developers give up on them over the noise. Johnson et al., ICSE 2013

WEEK 11Monday

goto fail

if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
    goto fail;
    goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
    goto fail;

CVE-2014-1266, iOS and macOS, February 2014

Key point

Some mistakes are too boring for people to catch. A tool does not get bored.

WEEK 11Monday

Project Pulse shipped type errors

  • vue-tsc: in Project Pulse since its first commit, October 2024
  • Fall 2026: 18 type errors on every deploy
  • The deploy ran build-only, which skips the checker
Key point

A tool that does not fail the build is decoration.

WEEK 11Monday

Fail fast

  • Static analysis: seconds, and the program never runs
  • Tests: minutes
  • A reviewer: an hour
Key point

If a cheap check can reject the change, don't spend the expensive one.

WEEK 11Monday

Three checks, three kinds of problem

Asks Cannot see The story
Static analysis Does any path break a known rule? What the code is for goto fail
Testing Does it do what we checked, on the inputs we ran? Cases nobody wrote CrowdStrike
Code review Is this what the use case asks for? Line 600 of a tired afternoon Heartbleed
Key point

Each one is blind where another looks.

WEEK 11Monday

Each slice stops something

Three slices of Swiss cheese: static analysis stops goto fail where it has no hole; a missing extension passes through aligned holes in static analysis and tests, and you, checking against the use case, stop it

Key point

Each slice stops what the others let through.

WEEK 11Monday

Why not just ask your coding agent?

Analyzer

Same code, same findings, every run.

Every file, in seconds, no tokens.

Blind to intent.

Model

Ask twice, get two answers.

Reads what fits in context, paid per run.

Can judge intent.

The agent

Veracode, 2025: 100+ AI models, 45% of their code failed security tests.

Key point

One loop: the analyzer finds, the agent fixes, the analyzer checks again.

WEEK 11Monday

Why every analyzer guesses

  • Can this line ever run? Undecidable: the halting problem reduces to it
  • Rice's theorem: every nontrivial question about what a program does is undecidable
  • So an analyzer that always finishes must approximate
Key point

No analyzer is both sound and complete. One that claims to be is claiming to solve the halting problem.

WEEK 11Monday

Two ways to be wrong

False positive

A warning on code that is fine. Costs trust.

Not complete.

False negative

A bug the tool never mentions. A clean report is not proof.

Not sound.

Heartbleed: Coverity missed it too.

Key point

Google's rule: if developers ignore more than 1 in 10 of a check's warnings, the check is fixed or turned off.

WEEK 11Monday

An analyzer that flags every line has zero false negatives.

WEEK 11Monday

Climb the ladder

A ladder of eight rungs, cheapest at the bottom: compiler and IDE, formatter, linter (where this module starts), null types and dataflow (4.9), then dependencies, containers, and secrets, beyond your own code (4.10)

WEEK 11Monday

What's a linter?

A static analyzer that checks code against a catalog of rules, and reports each match by rule, file, and line

  • Frontend: ESLint
  • Backend: PMD (source), SpotBugs (bytecode)

Named after lint (Bell Labs, 1978): the fluff a lint brush picks off your sweater.

WEEK 11Monday

Demo: the first run

Backend

./mvnw compile

./mvnw ...spotbugs-maven-plugin:4.10.5.0:spotbugs

./mvnw ...maven-pmd-plugin:3.28.0:pmd

Frontend

npx eslint . --ignore-path ../.gitignore

npm run type-check

npx prettier --check src/

WEEK 11Monday

Read by rule, not by file

Rule What it means Findings
EI_EXPOSE_REP2 a setter keeps a mutable object, not a copy 117
EI_EXPOSE_REP a getter hands out a mutable object 102
NP_NULL_ON_SOME_PATH... uses a result that may be null 14
CT_CONSTRUCTOR_THROW a constructor can throw halfway 3
DLS_DEAD_LOCAL_STORE a value stored, never used 1
Key point

The big rules are one decision. The bug is the one line at the bottom.

WEEK 11Monday

EI_EXPOSE_REP: sharing a mutable list

public List<Rating> getRatings() {
    return ratings;            // REP: hands out its own list
}

public void setRatings(List<Rating> ratings) {
    this.ratings = ratings;    // REP2: keeps the caller's list
    ...
    calculateTotalScore();
}
Key point

The caller adds a rating to the list. The total never changes.

WEEK 11Monday

CT_CONSTRUCTOR_THROW: a half-built object

public class Account {
    private final long balance;

    public Account(long balance) {
        if (balance < 0) throw new IllegalArgumentException();
        this.balance = balance;
    }
}
Key point

A subclass can rescue the half-built object. final closes the hole.

WEEK 11Monday

PMD, read the same way

Rule What it means Findings
UncommentedEmptyConstructor empty constructor, no comment why 30
ControlStatementBraces an if with no braces 30
UseLocaleWithCaseConversions toLowerCase() without a Locale 18
InstantiableUtilityClass static-only class you can new 12
UnnecessaryConstructor a constructor Java writes anyway 10
10 more rules literals first, == on objects, style... 30
Key point

74 of 130 are four project decisions. The bugs are in the small rules.

WEEK 11Monday

Braces: goto fail, waiting

if (!this.enabled) return; // Reminders are disabled
Key point

Correct today. The next person adds a second line under it.

WEEK 11Monday

toLowerCase and the Turkish i

"%" + providedFirstName.toLowerCase() + "%"
  • In Turkish, uppercase I lowercases to a dotless ı
  • A search for "Ivan" becomes "ıvan"; the database says "ivan"
  • No match. toLowerCase(Locale.ROOT) fixes it
WEEK 11Monday

Two decisions, yours to make

Empty constructors (40)

public Activity() {
}

JPA needs it. Comment it, or drop the rule?

GuardLogStatement (part of 30)

LOGGER.error("Could not send ... {} in section {}",
        student.getEmail(), section.getSectionName(), e);

SLF4J's {} already defers the string. Guard it anyway?

WEEK 11Monday

A private method nobody calls

private <T> void updateCollection(Set<T> oldCollection, Set<T> newCollection) {
    oldCollection.clear();
    ...
}
  • UseCaseService.updateCollection: private, and no caller anywhere
  • Dead code? Delete it
  • Or a call someone forgot? That is a bug
Key point

The tool sees the method is unused. Only the history says why.

WEEK 11Monday

Style, in one slide

Rule Before After
SimplifyBooleanReturns if (x == null) { return false; } return ok(x); return x != null && ok(x);
LambdaCanBeMethodReference a -> a.getAuthority() GrantedAuthority::getAuthority
UselessParentheses (MyUserPrincipal) (auth.getPrincipal()) (MyUserPrincipal) auth.getPrincipal()
UnnecessaryImport import ...UserDetails; (unused) delete it
InstantiableUtilityClass new StudentSpecs() compiles private StudentSpecs() {}
Key point

All true, all harmless. A team decides once, then a formatter or the agent does the rest.

WEEK 11Monday

Two tools, one overlap

SpotBugs reads bytecode: 237

Null paths, casts, comparisons, mutable objects escaping

PMD reads source: 130

Locale bugs, unused code, risky idioms, style

.anyMatch(a -> a.getAuthority().equals("ROLE_instructor"));

PMD: put the literal first. SpotBugs: getAuthority() may be null. Two rules, one worry: when both point at a line, look.

Key point

Found by both: 1 of 367, the dead store.

WEEK 11Monday

Three verdicts

Verdict Record it as
Real A fix, and a test if you can show the bug
False positive A suppression at the site, with the reason
Not worth it A suppression, or the rule off, with the reason
Pitfall

A verdict in your head gets made again, by the next person or the agent.

WEEK 11Monday

Real, but the fix needs a decision

@PatchMapping("/{activityId}/comments")
public Result addActivityComment(...) {
    String comment = commentMap.get("comment");
    Activity updatedActivity = this.activityService.addActivityComment(activityId, comment);
    ActivityDto updatedActivityDto = this.activityToActivityDtoConverter.convert(updatedActivity);
    return new Result(true, StatusCode.SUCCESS, "Add comment successfully", null);
}
Ask the room

Delete the line, or return the DTO? Who decides?

WEEK 11Monday

True by contract, false in context

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean hasInstructorRole = authentication.getAuthorities().stream() ...

Spring Security 7: @Nullable Authentication getAuthentication();

Ask the room

Behind the security filter chain it's never null. Is every caller behind it?

WEEK 11Monday

Deliberate, and the rule can't know

public void transferTeam(Team team, Section from, Section to) {
    if (team.getSection() != from) {
        throw new IllegalStateException("Team is not in the expected old section.");
    }
Pitfall

"Fix" this to .equals() and you change what it means.

WEEK 11Monday

Same rule, different verdict

try {
  const result = await searchRequirementArtifacts(...)
  glossaryTerms.value = result.data.content
} catch {
}

The Axios interceptor already showed the error.

WEEK 11Monday

Fix the configuration, not the code

  • 117 of ESLint's 130 problems: no-undef on cy, it, describe
  • The Cypress tests are fine; the linter was never told they are Cypress
  • One override in the ESLint configuration: 130 problems become 13
WEEK 11Monday

True, but not worth it

  • PMD's LocalVariableCouldBeFinal: right about every local it flags
  • Adding final everywhere changes no behavior if nobody reassigns locals
  • PMD's own quickstart ruleset ships it commented out
Key point

"Not worth it" needs a reason about this codebase, or it is a false positive nobody checked.

WEEK 11Monday

Suppress with a reason

@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
    justification = "Only called on authenticated routes")
// eslint-disable-next-line no-empty -- error already shown by the interceptor
Key point

A suppression without a reason is a bug report you deleted.

WEEK 11Monday

Make it a gate: where the checks run

Where What Why
Editor IntelliJ, SonarQube for IDE, ESLint Seconds
Before you push ./mvnw verify, frontend checks Same as the PR, a minute sooner
Pull request mvn verify, ESLint, Hadolint, CodeQL The gate
Schedule Dependabot, CodeQL on main New CVEs, old code
Key point

Shift left: the earlier the check, the cheaper the fix.

WEEK 11Monday

Check in CI, fix on your machine

  • Locally: eslint --fix, prettier --write
  • In CI: eslint, prettier --check, npm run type-check
  • Deploy with the checks too: build, never build-only
Key point

A CI job that fixes the code changes what it was asked to check.

WEEK 11Monday

Red has to block

  • A failing check stops a merge only if main requires it
  • GitHub: a ruleset (or branch protection) lists the required status checks
  • Without it, the merge button stays enabled
Key point

A red check nobody is required to fix is advice.

WEEK 11Monday

"My code has zero warnings." "How?" "I turned them off."

WEEK 11Monday

Baseline, then ratchet

  • Record what exists today as a baseline, untriaged
  • Fail the build only on findings outside it
  • Fix or suppress an old one, delete its entry
  • Never add to the baseline. It only shrinks
WEEK 11Monday

Project Pulse #104: the gate in pom.xml

<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.10.5.0</version>
  <configuration>
    <excludeFilterFile>${project.basedir}/config/spotbugs-exclude.xml,${project.basedir}/config/spotbugs-baseline.xml</excludeFilterFile>
  </configuration>
  <executions>
    <execution><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-pmd-plugin</artifactId>
  <version>3.28.0</version>
  <configuration>
    <rulesets><ruleset>${project.basedir}/config/pmd-ruleset.xml</ruleset></rulesets>
    <excludeFromFailureFile>${project.basedir}/config/pmd-baseline.properties</excludeFromFailureFile>
    <printFailingErrors>true</printFailingErrors>
  </configuration>
  <executions>
    <execution><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>

Baselines: 237 SpotBugs matches, 130 PMD findings in 68 classes. Frontend checks report only, for now; Actions pinned to SHAs.

Key point

check runs in verify, not test. New project? Drop the two baseline lines.

WEEK 11Monday

Demo: watch the gate fail

public boolean isAdmin(String role) {
    return role == "admin";
}
WEEK 11Monday

Know your baseline's grain

SpotBugs

Class, method, rule. A new finding in another method fails.

PMD

Class and rule. A new empty catch in a class that had one passes.

WEEK 11Monday

Upgrade, and format once

  • ESLint 8: end of life October 2024. An outdated analyzer is a finding too
  • Upgrade an analyzer in its own pull request; compare counts by rule
  • Upgrading PMD renamed a rule: 12 baselined findings came back as new
  • Format the whole codebase in one commit, alone, early
  • List that commit in .git-blame-ignore-revs, then gate prettier --check
WEEK 11Monday

Your project: gate first

  • Turn the gate on before the first feature
  • Triage the handful of findings a skeleton has
  • From then on, every finding is new, and fails
Key point

On new code, the best baseline is the one you never need.

WEEK 11Monday

Write it down where it lives

What you decided Where
Tools, gate command, how findings are settled Architecture-of-record, section 8.2
The rule the agent obeys Your charter, one line citing 8.2
A rule turned off, and why The tool's config, as a comment
A baseline Section 11, as debt
Pitfall

Not the specification, and not a key decision.

WEEK 11Monday

A convention no tool ships

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
Result handleOtherException(Exception ex) {
    return new Result(false, StatusCode.INTERNAL_SERVER_ERROR,
        "A server internal error occurs.", ex.getMessage());
}
Ask the room

Every unexpected exception ends here. What's missing?

WEEK 11Monday

The rule: a query over the syntax tree

//MethodDeclaration
  [ModifierList/Annotation[@SimpleName = 'ExceptionHandler']]
  [ModifierList/Annotation[@SimpleName = 'ResponseStatus']
     //FieldAccess[@Name = 'INTERNAL_SERVER_ERROR']]
  [not(.//MethodCall[@MethodName = ('error', 'warn')]
                    [*[1][matches(@Name, '^(log|logger|LOG|LOGGER)$')]])]

PMD custom rule, in backend/config/pmd-ruleset.xml

WEEK 11Monday

Demo: the rule, both ways

./mvnw verify -DskipTests: fails on handleOtherException

Add a logger and LOGGER.error("Unhandled exception", ex);: passes

Key point

A rule is code. Test it with one example it must flag and one it must pass.

WEEK 11Monday

A reviewer who writes the same comment three times is a linter with feelings.

WEEK 11Monday

Your own rules need triage too

  • First draft: "every catch that does not rethrow must log"
  • It flagged 1 of Project Pulse's 11 catch blocks: PathVariables.readId
  • That catch returns null on purpose: unresolvable means access denied
Key point

Narrow the rule, or suppress that site with its reason.

WEEK 11Monday

Same idea, other tools

Tool The rule is Good for
PMD XPath over the Java syntax tree Conventions, in your build
Semgrep A pattern that looks like the code Many languages
ArchUnit A JUnit test over packages "No controller calls a repository"
ESLint An AST selector in the config Frontend conventions
WEEK 11Monday

The agent drafts, you specify

The agent

Drafts the XPath or Semgrep rule in a minute.

You

Choose the convention. Write the two examples.

The agent

Writing a rule used to cost more than repeating the comment. Now it doesn't.

WEEK 11Monday

Null safety: JSpecify and NullAway

  • @NullMarked on a package: everything is non-null by default
  • @Nullable marks where null is legal
  • NullAway checks it at compile time, and fails the build
  • Spring Boot 4 is already annotated this way
WEEK 11Monday

Demo: NullAway on one package

public Integer getUserId() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth == null || !auth.isAuthenticated()) return null;
    ...
    return null;
}

NullAway: 14 errors in team.projectpulse.system

WEEK 11Monday

Undecidable becomes a type check

// CourseService
public @Nullable Course findCourseById(Integer courseId) {
    return this.courseRepository.findById(courseId).orElse(null);
}
// CourseController
Course course = this.courseService.findCourseById(courseId);
CourseDto courseDto = this.courseToCourseDtoConverter.convert(course);

CourseController.java:[46,71] passing @Nullable parameter 'course' where @NonNull is required

Key point

"Can this be null?" is undecidable. "Did you check the @Nullable value?" is not.

WEEK 11Monday

Tony Hoare called null "my billion-dollar mistake." NullAway is the refund desk.

WEEK 11Monday

Dataflow: source, sanitizer, sink

flowchart LR
  S[Source: request parameter] --> Z{Sanitizer?}
  Z -->|parameterized query| OK[Safe]
  Z -->|string concatenation| K[Sink: SQL string]

CodeQL default setup: free on public repositories, Java needs no build.

WEEK 11Monday

CodeQL's one finding

.csrf(csrf -> csrf.disable())

java/spring-disabled-csrf-protection · High · SecurityConfiguration

Ask the room

Is Project Pulse open to cross-site request forgery?

WEEK 11Monday

Most of your code is other people's

  • Project Pulse declares 26 dependencies; Maven resolves 263
  • Log4Shell, December 2021: the bug was in a library, not in anyone's own code
  • Dependabot: alerts on known CVEs, opens the update pull requests
WEEK 11Monday

A clean report can miss the point

FROM eclipse-temurin:21-jre
WORKDIR /application
COPY --from=builder /builder/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "application.jar"]

Hadolint: no findings. The app ran as root (fall 2026).

WEEK 11Monday

Your scanner is in your supply chain

Pitfall

March 2026: 75 of 76 trivy-action tags moved to code that stole pipeline secrets.

Nobody's workflow file changed. About twelve hours.

WEEK 11Monday

A tag is a promise, a SHA is a fact

Tag: the owner can move it

uses: actions/checkout@v4

SHA: nobody can

uses: actions/checkout@11d5960a... # v4.4.0

Dependabot proposes the next SHA; a person reviews it.

The best way into a secure pipeline is through its security scanner.

WEEK 11Monday

The agent and the analyzer

Delegate

Explain a rule. Fix the obvious ones and the style the team chose. Group the report. Write the test for a real one. Draft a custom rule.

Keep

Every suppression, every rule turned off, every baseline entry kept. Which conventions get a rule.

The agent

Charter: run ./mvnw verify before you say done; baselines only shrink; no suppression without a reason in the pull request.

WEEK 11Monday

Your week

  • Wednesday: CI/CD, these checks in the pipeline
  • Friday studio: first run on your repository, gate required on main, plant one bug, write it in 8.2; optional: one rule of your own
  • From now on, before every push: ./mvnw verify, not ./mvnw test
  • Assignment 4, Triage the analyzers: released Wednesday, due date on the schedule
Key point

Fix it, suppress it with a reason, or turn the rule off with a reason. Never just hide it.

Static Analysis

→ space PgDn
Next step or slide
← PgUp
Back
↓ S
Skip to the next slide
O
Overview of all slides
N
Speaker notes
B
Black the screen
Ctrl+P
Print / PDF handout, one slide per page