Skip to content

[linter-miner] feat(linters): add uncheckedtypeassertion linter (run #18)#34738

Merged
pelikhan merged 5 commits into
mainfrom
linter-miner/uncheckedtypeassertion-1dd60cf06ba5664b
May 25, 2026
Merged

[linter-miner] feat(linters): add uncheckedtypeassertion linter (run #18)#34738
pelikhan merged 5 commits into
mainfrom
linter-miner/uncheckedtypeassertion-1dd60cf06ba5664b

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented May 25, 2026

Summary

Adds a new uncheckedtypeassertion static analysis linter to gh-aw that detects single-value type assertions (x.(T)) that may panic at runtime and recommends the safe two-value form (v, ok := x.(T)). Motivated by issue #34580, where a production panic was caused by project["id"].(string) when the API returned nil.


What Changed

New Linter: uncheckedtypeassertion

pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go (added, high impact)

  • Implements a golang.org/x/tools/go/analysis analyzer that walks each file's AST looking for *ast.TypeAssertExpr nodes.
  • Builds a per-file parent map via ast.Inspect for O(1) parent lookup during traversal.
  • Safe patterns skipped:
    • Type-switch guards (TypeAssertExpr.Type == nil)
    • Two-value assignments: parent is *ast.AssignStmt with len(Lhs) == 2 && len(Rhs) == 1
    • Test files (via filecheck.IsTestFile)
  • Flagged pattern: any remaining single-value type assertion that can panic at runtime.
  • Depends on inspect.Analyzer for efficient AST traversal.

pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion_test.go (added, medium impact)

  • Wires the new analyzer to analysistest to validate correct diagnostic reporting against the testdata fixtures.

pkg/linters/uncheckedtypeassertion/testdata/src/uncheckedtypeassertion/uncheckedtypeassertion.go (added, low impact)

  • Fixture file covering all key cases:
    • ✅ Safe two-value assertions (v, ok := x.(T))
    • ✅ Safe type switches
    • ❌ Unsafe single-value assertions (expected to be flagged)

Registration

cmd/linters/main.go (modified, medium impact)

  • Registered the uncheckedtypeassertion analyzer with the linters binary so it runs as part of the standard linter suite.

Documentation

docs/adr/34738-add-uncheckedtypeassertion-linter.md (added, low impact)


Motivation

Issue #34580 surfaced a runtime panic from project["id"].(string) when the GitHub API returned nil for that key. Single-value type assertions are idiomatic Go but silently dangerous when the asserted type is not guaranteed. This linter catches the pattern at analysis time before it reaches production.


Impact Assessment

Area Impact Breaking
New linter implementation High No
Linter binary registration Medium No
Test coverage Medium No
Testdata fixtures Low No
ADR documentation Low No

No breaking changes. Existing code that already uses the two-value form or type switches is unaffected.


Commit History

SHA Message
a6756c32f feat(linters): add uncheckedtypeassertion linter
8902d80c3 docs(adr): add draft ADR-34738 for uncheckedtypeassertion linter
22dece503 fix(linters): satisfy Go lint in uncheckedtypeassertion analyzer
2cc06da31 fix(linters): satisfy Go lint in uncheckedtypeassertion analyzer

Generated by PR Description Updater for issue #34738 · sonnet46 1.2M ·

Reports single-value type assertions x.(T) that may panic at runtime
when the dynamic type does not match. Flags cases where the safe
two-value form v, ok := x.(T) should be used instead.

Evidence: issue #34580 — unchecked type assertion in GraphQL response
parsing caused a panic on unexpected nil/wrong-type values, while
sibling fields used the safe ok idiom.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! go-linters labels May 25, 2026
@pelikhan pelikhan marked this pull request as ready for review May 25, 2026 18:09
Copilot AI review requested due to automatic review settings May 25, 2026 18:09
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot lint go

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 25, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 25, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 25, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 25, 2026

🧪 Test Quality Sentinel completed test quality analysis.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new custom Go go/analysis analyzer (uncheckedtypeassertion) to flag single-value type assertions (x.(T)) in non-test code, encouraging the safer two-result form to avoid runtime panics.

Changes:

  • Introduces uncheckedtypeassertion analyzer implementation and wiring.
  • Adds analysistest coverage with fixtures for good/bad assertion patterns.
  • Registers the analyzer in cmd/linters so it runs with the existing multichecker binary.
Show a summary per file
File Description
pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go Implements the analyzer logic to detect unchecked single-value type assertions.
pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion_test.go Adds an analysistest harness for the new analyzer.
pkg/linters/uncheckedtypeassertion/testdata/src/uncheckedtypeassertion/uncheckedtypeassertion.go Provides good/bad fixtures with // want expectations.
cmd/linters/main.go Registers the new analyzer in the linter multichecker.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go:66

  • The “safe two-value form” skip logic only handles *ast.AssignStmt (e.g. v, ok := x.(T)), but var v, ok = x.(T) is also the safe two-result form and will be incorrectly reported. Consider also recognizing a parent *ast.ValueSpec (and verifying this TypeAssertExpr is the sole RHS value) as a safe two-value usage.
// Skip the safe two-value form:  v, ok := x.(T)  or  v, ok = x.(T)
if parents != nil {
if assign, ok := parents[typeAssert].(*ast.AssignStmt); ok {
if len(assign.Lhs) == 2 && len(assign.Rhs) == 1 {
return
}
}
  • Files reviewed: 4/4 changed files
  • Comments generated: 3

Comment on lines +7 to +102
"go/ast"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"

"github.com/github/gh-aw/pkg/linters/internal/filecheck"
)

// Analyzer is the unchecked-type-assertion analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "uncheckedtypeassertion",
Doc: "reports single-value type assertions that may panic if the dynamic type does not match",
URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/uncheckedtypeassertion",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}

func run(pass *analysis.Pass) (any, error) {
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

// Build a parent map for each file so we can detect the two-value form.
fileParents := make(map[*ast.File]map[ast.Node]ast.Node)
for _, f := range pass.Files {
fileParents[f] = buildParentMap(f)
}

nodeFilter := []ast.Node{
(*ast.TypeAssertExpr)(nil),
}

insp.Preorder(nodeFilter, func(n ast.Node) {
typeAssert := n.(*ast.TypeAssertExpr)

// Type-switch guards have nil Type; skip them.
if typeAssert.Type == nil {
return
}

pos := pass.Fset.PositionFor(typeAssert.Pos(), false)
if filecheck.IsTestFile(pos.Filename) {
return
}

// Find the parent map for the file containing this node.
var parents map[ast.Node]ast.Node
for _, f := range pass.Files {
if f.Pos() <= typeAssert.Pos() && typeAssert.Pos() <= f.End() {
parents = fileParents[f]
break
}
}

// Skip the safe two-value form: v, ok := x.(T) or v, ok = x.(T)
if parents != nil {
if assign, ok := parents[typeAssert].(*ast.AssignStmt); ok {
if len(assign.Lhs) == 2 && len(assign.Rhs) == 1 {
return
}
}
}

t := pass.TypesInfo.TypeOf(typeAssert.Type)
if t == nil {
return
}
pass.ReportRangef(
typeAssert,
"type assertion x.(%s) is unchecked and may panic; use the two-value form v, ok := x.(%s) instead",
t, t,
)
})

return nil, nil
}

// buildParentMap constructs a map from each AST node to its direct parent node.
func buildParentMap(root ast.Node) map[ast.Node]ast.Node {
parents := make(map[ast.Node]ast.Node)
var stack []ast.Node

ast.Inspect(root, func(n ast.Node) bool {
if n == nil {
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
return false
}
if len(stack) > 0 {
parents[n] = stack[len(stack)-1]
}
stack = append(stack, n)
return true
})

return parents
Comment on lines +5 to +19
// Good: two-value type assertion is safe.
func GoodTwoValue(v interface{}) {
s, ok := v.(string)
if ok {
fmt.Println(s)
}
}

// Good: type switch is safe — not flagged.
func GoodTypeSwitch(v interface{}) {
switch t := v.(type) {
case string:
fmt.Println(t)
}
}
Comment on lines +1 to +3
// Package uncheckedtypeassertion implements a Go analysis linter that flags
// single-value type assertions x.(T) that may panic at runtime if the dynamic
// type does not match, and where the two-value safe form x.(T) is not used.
Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — commenting with suggestions, no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Use inspector.WithStack: The hand-rolled buildParentMap + per-assertion O(N files) lookup can be replaced with the built-in insp.WithStack, which is both simpler and more efficient.
  • Fixture gaps: Argument-position assertions (foo(v.(T))) and struct-field-initializer forms are untested. The message "use the two-value form" is also misleading for contexts where that form is syntactically unavailable.
  • Style: Test fixtures use interface{} instead of any, inconsistent with the project's Go 1.18+ style rule.

Positive Highlights

  • ✅ Correct type-switch exclusion (typeAssert.Type == nil) — shows solid understanding of the Go AST
  • ✅ Test-file exclusion via filecheck.IsTestFile is consistent with sibling linters
  • ✅ Motivated by a real runtime panic (issue #34580) — good signal-to-noise for the linter category
  • ✅ Clean analysis.Analyzer struct with URL field populated
  • analysistest-based test harness with // want annotations

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.3M

return
}

// Find the parent map for the file containing this node.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The per-file parent-map lookup is O(N files) for each type assertion node. Consider using inspector.WithStack instead — it provides the ancestor stack directly during traversal without requiring a pre-built map.

💡 Suggested refactor

Replace the fileParents pre-build and the parent-lookup loop with insp.WithStack:

insp.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
    if !push {
        return true
    }
    typeAssert, ok := n.(*ast.TypeAssertExpr)
    if !ok || typeAssert.Type == nil {
        return true
    }
    // parent is stack[len(stack)-2]
    if len(stack) >= 2 {
        if assign, ok := stack[len(stack)-2].(*ast.AssignStmt); ok {
            if len(assign.Lhs) == 2 {
                return true // safe two-value form
            }
        }
    }
    // ... report
    return true
})

This eliminates the buildParentMap function entirely, removes the O(files) scan per node, and avoids the potential correctness gap where the file-boundary check (f.Pos() <= ... <= f.End()) could silently fail to find a parent map.

func GoodTwoValueBlankOk(v interface{}) string {
s, _ := v.(string)
return s
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Missing fixture: type assertion in a function-call argument position (e.g. foo(v.(string))) is a common real-world pattern and the current fixtures don't confirm whether it is correctly flagged.

💡 Suggested test cases to add
// Bad: assertion passed directly as a function argument
func BadArgPosition(v interface{}) {
    fmt.Println(v.(string)) // want `type assertion x\.\(string\) is unchecked and may panic`
}

// Bad: assertion in a return expression with no comma-ok
func BadReturnExpr(v interface{}) string {
    return v.(string) // want `type assertion x\.\(string\) is unchecked and may panic`
}

// Good: explicit type switch with default branch
func GoodTypeSwitchDefault(v interface{}) {
    switch v.(type) {
    case string:
    default:
    }
}

Argument-position assertions are especially risky because the two-value form isn't syntactically available there, making them a source of unavoidable panics that deserve an explicit // nolint or a refactor.

}
pass.ReportRangef(
typeAssert,
"type assertion x.(%s) is unchecked and may panic; use the two-value form v, ok := x.(%s) instead",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Correctness gap: argument-position assertions like foo(v.(T)) have no parent *ast.AssignStmt, so the two-value check (len(assign.Lhs) == 2) never fires. The analyzer will correctly report them — but there is no fixture confirming this. More importantly, v, ok := someFunc(x.(T)) (assertion inside a function call that itself is the RHS of a two-value assignment) would not be caught by the current parent check, which only looks one level up.

💡 Detail

The current guard:

if assign, ok := parents[typeAssert].(*ast.AssignStmt); ok {
    if len(assign.Lhs) == 2 && len(assign.Rhs) == 1 {
        return
    }
}

only skips the assertion when the direct parent is an assignment. If the assertion is nested (e.g. results, ok := db.Query(v.(string))), the assertion's direct parent is a *ast.CallExpr, not an *ast.AssignStmt, so it is correctly flagged — but the error message says "use the two-value form" which is not possible in that position. Consider detecting argument-position assertions and emitting a different, more actionable message (e.g., "extract the assertion before the call").


import "fmt"

// Good: two-value type assertion is safe.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Testdata uses interface{} instead of any. The project style guide (AGENTS.md) mandates any over interface{} for Go 1.18+ code. The linter itself should arguably flag interface{} usages via a separate linter, but at minimum the fixture file should model the preferred style.

💡 One-line fix

Replace interface{} with any throughout the test fixtures:

func GoodTwoValue(v any) {
func BadSingleValue(v any) string {
// etc.

@github-actions
Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test: 1 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestAnalyzer pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion_test.go:13 ✅ Design None

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%).

TestAnalyzer uses the idiomatic analysistest.Run pattern for go/analysis linters. Assertions are expressed as // want annotations in the testdata fixture, covering 2 bad cases (BadSingleValue, BadSingleValueAssign — unchecked type assertions that may panic) and 3 good cases (GoodTwoValue, GoodTypeSwitch, GoodTwoValueBlankOk — safe patterns). This directly verifies the linter's behavioral contract. The mandatory //go:build !integration build tag is present. Test inflation ratio is well within bounds (16 test lines vs. 103 production lines).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1.7M ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%).

Generated by the Design Decision Gate. Documents the decision to add a
new in-house static analyzer that flags single-value type assertions
x.(T) which may panic at runtime, recommending the safe two-value
form v, ok := x.(T) instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor Author

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (155 new lines in pkg/, exceeding the 100-line threshold) but did not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/34738-add-uncheckedtypeassertion-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff and the evidence cited in the description (issue Unchecked type assertion in pkg/cli/project_command.go — panics on malformed GraphQL response (#aw_sg18a1) #34580, pattern scan across pkg/).
  2. Complete the missing sections — confirm or refine the context, decision rationale, and trade-offs the AI inferred. In particular, validate:
    • The structural-only detection trade-off (no go/types narrowing) in the Negative consequences.
    • The list of pre-existing call sites that won't be fixed in this PR.
    • Whether type-switch and test-file exclusions are exhaustive for your intent.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-34738: Add uncheckedtypeassertion Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 34738-*.md for PR #34738).

References: §26413798298

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus47 5M ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — three issues must be fixed before merge; one will break CI immediately.

### Blocking issues (3)
  1. Missing gofmt indentation (CI-breaking) — the entire uncheckedtypeassertion.go body is at column 0; make fmt will fail the format gate before any other check runs.

  2. False positive on var s, ok = v.(T) — the safe two-value var declaration form has a *ast.ValueSpec parent, not *ast.AssignStmt, so it will be incorrectly reported as unsafe. The parent-type guard needs a second branch for *ast.ValueSpec.

  3. insp.WithStack vs manual fileParents + inner loop — the O(assertions × files) scan inside the hot Preorder callback should be replaced with insp.WithStack, which is the idiomatic approach used elsewhere in this package, eliminates buildParentMap, and provides the parent for free.

Test coverage gaps (missing CallExpr-parent and blank-assign cases) are noted inline.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 2M


// Analyzer is the unchecked-type-assertion analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "uncheckedtypeassertion",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entire file body lacks gofmt indentation — CI will fail the format check immediately.

Every line inside function bodies, struct literals, and the import block is missing the leading tab(s) that gofmt requires. Compare to pkg/linters/ssljson/ssljson.go where struct fields and function bodies are tab-indented.

💡 Fix

Run gofmt -w pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go (or make fmt). Example of what the struct should look like:

var Analyzer = &analysis.Analyzer{
	Name:     "uncheckedtypeassertion",
	Doc:      "reports single-value type assertions ...",
	Requires: []*analysis.Analyzer{inspect.Analyzer},
	Run:      run,
}

The CI make fmt gate will reject this file as-is.


// Skip the safe two-value form: v, ok := x.(T) or v, ok = x.(T)
if parents != nil {
if assign, ok := parents[typeAssert].(*ast.AssignStmt); ok {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: var s, ok = v.(T) is safe but will be incorrectly flagged — the two-value check only handles *ast.AssignStmt, missing *ast.ValueSpec.

💡 Details and fix

The safe two-value declaration form:

var s, ok = v.(string)  // safe — ValueSpec parent, NOT AssignStmt

has a *ast.ValueSpec as the parent node, not *ast.AssignStmt. The current guard:

if assign, ok := parents[typeAssert].(*ast.AssignStmt); ok {
    if len(assign.Lhs) == 2 ...

will not match, so this valid safe pattern gets falsely reported as unsafe.

Fix — also check for *ast.ValueSpec with two names:

if spec, ok := parents[typeAssert].(*ast.ValueSpec); ok {
    if len(spec.Names) == 2 {
        return
    }
}

Also add a testdata case:

// Good: var declaration two-value form is safe.
func GoodVarDecl(v interface{}) {
    var s, ok = v.(string)
    if ok {
        fmt.Println(s)
    }
}

insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

// Build a parent map for each file so we can detect the two-value form.
fileParents := make(map[*ast.File]map[ast.Node]ast.Node)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O(assertions × files) inner loop — use insp.WithStack instead, which is the idiomatic approach used by other linters in this package.

💡 Details and fix

The code pre-builds fileParents (one full AST walk per file), then for every TypeAssertExpr node iterates pass.Files again to find which file it belongs to. In a package with F files and A type assertions that's O(F×A) iterations plus the O(nodes-per-file) build cost — all wasted when no assertions are found.

The idiomatic fix is insp.WithStack, which provides the ancestor stack on-demand with zero pre-allocation:

insp.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
    if !push {
        return true
    }
    typeAssert := n.(*ast.TypeAssertExpr)
    if typeAssert.Type == nil {
        return true
    }
    // stack[0] is always the *ast.File — no file-range loop needed
    // Parent is stack[len(stack)-2]
    parent := stack[len(stack)-2]
    if assign, ok := parent.(*ast.AssignStmt); ok && len(assign.Lhs) == 2 {
        return true // safe two-value form
    }
    ...
})

This eliminates fileParents, buildParentMap, and the per-node file scan entirely.

func GoodTwoValueBlankOk(v interface{}) string {
s, _ := v.(string)
return s
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for assertion passed as a function argument — the most common real-world pattern is not exercised.

💡 Suggested additions

Add at least these two cases:

// Bad: assertion as function call argument.
func BadCallArg(v interface{}) {
    fmt.Println(v.(string)) // want `type assertion x\.\(string\) is unchecked and may panic`
}

// Bad: single-value blank assign still panics.
func BadBlankAssign(v interface{}) {
    _ = v.(string) // want `type assertion x\.\(string\) is unchecked and may panic`
}

The CallExpr-parent path through the parent-lookup logic is untested; a regression there would silently produce false negatives for a pattern that appears frequently in real code.

Copilot AI and others added 2 commits May 25, 2026 18:19
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 25, 2026

@copilot lint go

Ran Go lint and fixed the reported issues in commit 2cc06da. Verified with make golint and go test ./pkg/linters/uncheckedtypeassertion/....

Copilot AI requested a review from pelikhan May 25, 2026 18:21
@pelikhan pelikhan merged commit ee6854f into main May 25, 2026
@pelikhan pelikhan deleted the linter-miner/uncheckedtypeassertion-1dd60cf06ba5664b branch May 25, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants