a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
278 lines
8.1 KiB
Go
278 lines
8.1 KiB
Go
// Package entrypoints detects framework entry points — symbols and
|
|
// files reachable only from a framework runtime, never from
|
|
// application code. Alembic migrations, Next.js pages / routes, and
|
|
// ASP.NET host files all look unreachable to a call-graph walk; left
|
|
// unmarked they become dead-code false positives. Detect stamps
|
|
// Meta["entry_point"] / Meta["entry_point_kind"] so the dead-code
|
|
// analyzer treats them as live roots.
|
|
package entrypoints
|
|
|
|
import (
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// MetaEntryPoint / MetaEntryKind are the Node.Meta keys this package
|
|
// stamps. Exported so the dead-code analyzer can read them.
|
|
const (
|
|
MetaEntryPoint = "entry_point"
|
|
MetaEntryKind = "entry_point_kind"
|
|
)
|
|
|
|
// Detect inspects one file's extracted nodes (and edges, for the
|
|
// annotation-driven detectors) for framework entry points and stamps
|
|
// the file node plus the relevant symbols. It returns the number of
|
|
// nodes stamped.
|
|
func Detect(relPath, lang string, nodes []*graph.Node, edges []*graph.Edge) int {
|
|
slashed := path.Clean(strings.ReplaceAll(relPath, "\\", "/"))
|
|
switch lang {
|
|
case "python":
|
|
return detectAlembic(nodes)
|
|
case "typescript", "javascript":
|
|
return detectNextJS(slashed, nodes)
|
|
case "csharp":
|
|
return detectASPNet(slashed, nodes)
|
|
case "java":
|
|
return detectJava(nodes, edges)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// stamp marks a node as a framework entry point.
|
|
func stamp(n *graph.Node, kind string) {
|
|
if n.Meta == nil {
|
|
n.Meta = map[string]any{}
|
|
}
|
|
n.Meta[MetaEntryPoint] = true
|
|
n.Meta[MetaEntryKind] = kind
|
|
}
|
|
|
|
func isFnOrMethod(k graph.NodeKind) bool {
|
|
return k == graph.KindFunction || k == graph.KindMethod
|
|
}
|
|
|
|
// detectAlembic flags an Alembic migration: a Python module defining
|
|
// both upgrade() and downgrade() at module level. That pair is the
|
|
// distinctive migration signature (Alembic, Flask-Migrate, …) and is
|
|
// path-independent — Alembic version directories are configurable.
|
|
func detectAlembic(nodes []*graph.Node) int {
|
|
var hasUpgrade, hasDowngrade bool
|
|
for _, n := range nodes {
|
|
switch {
|
|
case n.Kind == graph.KindFunction && n.Name == "upgrade":
|
|
hasUpgrade = true
|
|
case n.Kind == graph.KindFunction && n.Name == "downgrade":
|
|
hasDowngrade = true
|
|
}
|
|
}
|
|
if !hasUpgrade || !hasDowngrade {
|
|
return 0
|
|
}
|
|
count := 0
|
|
for _, n := range nodes {
|
|
if n.Kind == graph.KindFile ||
|
|
(n.Kind == graph.KindFunction && (n.Name == "upgrade" || n.Name == "downgrade")) {
|
|
stamp(n, "alembic:migration")
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// nextAppSpecialFiles are the Next.js App Router special filenames.
|
|
var nextAppSpecialFiles = map[string]bool{
|
|
"page": true, "layout": true, "route": true, "loading": true,
|
|
"error": true, "not-found": true, "template": true,
|
|
"default": true, "global-error": true,
|
|
}
|
|
|
|
// nextEntrySymbols are exported functions the Next.js runtime calls
|
|
// directly — they have no in-app caller.
|
|
var nextEntrySymbols = map[string]bool{
|
|
"getServerSideProps": true,
|
|
"getStaticProps": true,
|
|
"getStaticPaths": true,
|
|
"generateStaticParams": true,
|
|
"generateMetadata": true,
|
|
"middleware": true,
|
|
}
|
|
|
|
// detectNextJS flags Next.js pages, API routes, and App Router special
|
|
// files. App Router detection keys on the distinctive special
|
|
// filenames so a generic `app/` directory does not over-match.
|
|
func detectNextJS(relPath string, nodes []*graph.Node) int {
|
|
ext := path.Ext(relPath)
|
|
switch ext {
|
|
case ".js", ".jsx", ".ts", ".tsx":
|
|
default:
|
|
return 0
|
|
}
|
|
segs := strings.Split(relPath, "/")
|
|
base := strings.TrimSuffix(path.Base(relPath), ext)
|
|
|
|
kind := ""
|
|
switch {
|
|
case slices.Contains(segs, "app") && nextAppSpecialFiles[base]:
|
|
if base == "route" {
|
|
kind = "nextjs:route"
|
|
} else {
|
|
kind = "nextjs:page"
|
|
}
|
|
case slices.Contains(segs, "pages"):
|
|
if slices.Contains(segs, "api") {
|
|
kind = "nextjs:api"
|
|
} else {
|
|
kind = "nextjs:page"
|
|
}
|
|
default:
|
|
return 0
|
|
}
|
|
|
|
count := 0
|
|
for _, n := range nodes {
|
|
if n.Kind == graph.KindFile {
|
|
stamp(n, kind)
|
|
count++
|
|
continue
|
|
}
|
|
if isFnOrMethod(n.Kind) && nextEntrySymbols[n.Name] {
|
|
stamp(n, kind)
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// detectASPNet flags an ASP.NET host file — Program.cs / Startup.cs —
|
|
// and its lifecycle methods (Main / ConfigureServices / Configure),
|
|
// which the host invokes, not application code.
|
|
func detectASPNet(relPath string, nodes []*graph.Node) int {
|
|
switch path.Base(relPath) {
|
|
case "Program.cs", "Startup.cs":
|
|
default:
|
|
return 0
|
|
}
|
|
count := 0
|
|
for _, n := range nodes {
|
|
switch {
|
|
case n.Kind == graph.KindFile:
|
|
stamp(n, "aspnet:host")
|
|
count++
|
|
case isFnOrMethod(n.Kind) &&
|
|
(n.Name == "Main" || n.Name == "ConfigureServices" || n.Name == "Configure"):
|
|
stamp(n, "aspnet:host")
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// javaAnnoPrefix is the synthetic annotation node-ID prefix the Java
|
|
// extractor emits (languages.AnnotationNodeID("java", name)); the bare
|
|
// annotation name is the suffix.
|
|
const javaAnnoPrefix = "annotation::java::"
|
|
|
|
// javaEntryClassAnnos maps a class/interface-level annotation to the
|
|
// entry-point kind it confers. These mark a *type* the framework
|
|
// instantiates and drives (Spring stereotypes, JAX-RS resources,
|
|
// annotated servlets / websocket endpoints).
|
|
var javaEntryClassAnnos = map[string]string{
|
|
"RestController": "spring:controller",
|
|
"Controller": "spring:controller",
|
|
"Service": "spring:bean",
|
|
"Component": "spring:bean",
|
|
"Repository": "spring:bean",
|
|
"Configuration": "spring:config",
|
|
"Path": "jaxrs:resource",
|
|
"WebServlet": "servlet:endpoint",
|
|
"ServerEndpoint": "websocket:endpoint",
|
|
}
|
|
|
|
// javaEntryMethodAnnos maps a method-level annotation to the entry-point
|
|
// kind it confers. These mark a *method* the framework invokes directly
|
|
// — request handlers, lifecycle callbacks, scheduled jobs, test cases —
|
|
// which therefore has no in-application caller.
|
|
var javaEntryMethodAnnos = map[string]string{
|
|
"RequestMapping": "spring:handler",
|
|
"GetMapping": "spring:handler",
|
|
"PostMapping": "spring:handler",
|
|
"PutMapping": "spring:handler",
|
|
"DeleteMapping": "spring:handler",
|
|
"PatchMapping": "spring:handler",
|
|
"EventListener": "spring:listener",
|
|
"Scheduled": "spring:scheduled",
|
|
"Bean": "spring:bean",
|
|
"PostConstruct": "lifecycle:init",
|
|
"PreDestroy": "lifecycle:destroy",
|
|
"Test": "junit:test",
|
|
"ParameterizedTest": "junit:test",
|
|
"RepeatedTest": "junit:test",
|
|
"BeforeEach": "junit:fixture",
|
|
"AfterEach": "junit:fixture",
|
|
"BeforeAll": "junit:fixture",
|
|
"AfterAll": "junit:fixture",
|
|
"GET": "jaxrs:handler",
|
|
"POST": "jaxrs:handler",
|
|
"PUT": "jaxrs:handler",
|
|
"DELETE": "jaxrs:handler",
|
|
"HEAD": "jaxrs:handler",
|
|
}
|
|
|
|
// detectJava flags Java framework entry points from annotation edges:
|
|
// Spring stereotypes / request handlers, JAX-RS resources, annotated
|
|
// servlets, JUnit test methods, lifecycle callbacks, and the JVM
|
|
// `main` method. Unlike the path-based detectors it stamps the
|
|
// individual annotated symbols, NOT the file node — a Spring controller
|
|
// file can still hold genuinely-dead private helpers, and only the
|
|
// framework-invoked members should be treated as live roots.
|
|
func detectJava(nodes []*graph.Node, edges []*graph.Edge) int {
|
|
// symbol ID → set of annotation names applied to it.
|
|
annos := map[string]map[string]bool{}
|
|
for _, e := range edges {
|
|
if e.Kind != graph.EdgeAnnotated {
|
|
continue
|
|
}
|
|
name, ok := strings.CutPrefix(e.To, javaAnnoPrefix)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if annos[e.From] == nil {
|
|
annos[e.From] = map[string]bool{}
|
|
}
|
|
annos[e.From][name] = true
|
|
}
|
|
|
|
count := 0
|
|
for _, n := range nodes {
|
|
switch n.Kind {
|
|
case graph.KindType, graph.KindInterface:
|
|
for a := range annos[n.ID] {
|
|
if kind, ok := javaEntryClassAnnos[a]; ok {
|
|
stamp(n, kind)
|
|
count++
|
|
break
|
|
}
|
|
}
|
|
case graph.KindFunction, graph.KindMethod:
|
|
kind := ""
|
|
for a := range annos[n.ID] {
|
|
if k, ok := javaEntryMethodAnnos[a]; ok {
|
|
kind = k
|
|
break
|
|
}
|
|
}
|
|
if kind == "" && n.Name == "main" {
|
|
kind = "java:main"
|
|
}
|
|
if kind != "" {
|
|
stamp(n, kind)
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
return count
|
|
}
|