diff --git a/bins/codefang/tests/fixtures/shotness_languages/bash.sh b/bins/codefang/tests/fixtures/shotness_languages/bash.sh new file mode 100644 index 00000000..c37efb1b --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/bash.sh @@ -0,0 +1 @@ +probe() { echo 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/c.c b/bins/codefang/tests/fixtures/shotness_languages/c.c new file mode 100644 index 00000000..7f328f4c --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/c.c @@ -0,0 +1 @@ +int probe(void) { return 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/c_sharp.cs b/bins/codefang/tests/fixtures/shotness_languages/c_sharp.cs new file mode 100644 index 00000000..6c0a3473 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/c_sharp.cs @@ -0,0 +1 @@ +class Box { int probe() { return 111; } } diff --git a/bins/codefang/tests/fixtures/shotness_languages/clojure.clj b/bins/codefang/tests/fixtures/shotness_languages/clojure.clj new file mode 100644 index 00000000..fd8e880c --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/clojure.clj @@ -0,0 +1 @@ +(defn probe [x] (+ x 111)) diff --git a/bins/codefang/tests/fixtures/shotness_languages/clojure_forms.clj b/bins/codefang/tests/fixtures/shotness_languages/clojure_forms.clj new file mode 100644 index 00000000..a78da8bb --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/clojure_forms.clj @@ -0,0 +1,10 @@ +(defn- private-probe [x] + (+ x 111)) +(defmacro macro-probe [x] + `(+ ~x 111)) +(require '[clojure.test :refer [deftest is]]) +(deftest test-probe + (is (= 111 111))) +(defmulti method-probe identity) +(defmethod method-probe :default [x] + (+ x 111)) diff --git a/bins/codefang/tests/fixtures/shotness_languages/cmake.cmake b/bins/codefang/tests/fixtures/shotness_languages/cmake.cmake new file mode 100644 index 00000000..5bd436d7 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/cmake.cmake @@ -0,0 +1,3 @@ +function(probe) + message(STATUS "111") +endfunction() diff --git a/bins/codefang/tests/fixtures/shotness_languages/cmake_macro.cmake b/bins/codefang/tests/fixtures/shotness_languages/cmake_macro.cmake new file mode 100644 index 00000000..ba38e179 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/cmake_macro.cmake @@ -0,0 +1,3 @@ +macro(probe) + message(STATUS "111") +endmacro() diff --git a/bins/codefang/tests/fixtures/shotness_languages/commonlisp.lisp b/bins/codefang/tests/fixtures/shotness_languages/commonlisp.lisp new file mode 100644 index 00000000..cd54269f --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/commonlisp.lisp @@ -0,0 +1,2 @@ +(defun probe (x) + (+ x 111)) diff --git a/bins/codefang/tests/fixtures/shotness_languages/commonlisp_forms.lisp b/bins/codefang/tests/fixtures/shotness_languages/commonlisp_forms.lisp new file mode 100644 index 00000000..cdc753e3 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/commonlisp_forms.lisp @@ -0,0 +1,6 @@ +(defmacro macro-probe (x) + `(+ ,x 111)) +(defgeneric generic-probe (x) + (:documentation "111")) +(defmethod method-probe ((x integer)) + (+ x 111)) diff --git a/bins/codefang/tests/fixtures/shotness_languages/cpp.cpp b/bins/codefang/tests/fixtures/shotness_languages/cpp.cpp new file mode 100644 index 00000000..9e9aa008 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/cpp.cpp @@ -0,0 +1 @@ +int probe() { return 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/crystal.cr b/bins/codefang/tests/fixtures/shotness_languages/crystal.cr new file mode 100644 index 00000000..30cef7d2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/crystal.cr @@ -0,0 +1,3 @@ +def probe(x) + x + 111 +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/css.css b/bins/codefang/tests/fixtures/shotness_languages/css.css new file mode 100644 index 00000000..9971fc90 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/css.css @@ -0,0 +1,2 @@ +:root { --value: 111; } +p { width: calc(var(--value) * 1px); } diff --git a/bins/codefang/tests/fixtures/shotness_languages/dart.dart b/bins/codefang/tests/fixtures/shotness_languages/dart.dart new file mode 100644 index 00000000..c0ea552d --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/dart.dart @@ -0,0 +1 @@ +int probe(int x) { return x + 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/elixir.ex b/bins/codefang/tests/fixtures/shotness_languages/elixir.ex new file mode 100644 index 00000000..892991ad --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/elixir.ex @@ -0,0 +1,5 @@ +defmodule Box do + def probe(x) do + x + 111 + end +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/elixir_forms.ex b/bins/codefang/tests/fixtures/shotness_languages/elixir_forms.ex new file mode 100644 index 00000000..8941a536 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/elixir_forms.ex @@ -0,0 +1,14 @@ +defmodule Box do + defp private_probe(x) do + x + 111 + end + defmacro macro_probe(x) do + 111 + end + defmacrop private_macro(x) do + 111 + end + def zero_probe do + 111 + end +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/elixir_guarded.ex b/bins/codefang/tests/fixtures/shotness_languages/elixir_guarded.ex new file mode 100644 index 00000000..c6c5d3b9 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/elixir_guarded.ex @@ -0,0 +1,14 @@ +defmodule Box do + def guarded(x) when is_integer(x) do + 111 + end + defp private_guarded(x) when is_integer(x) do + 111 + end + defmacro macro_guarded(x) when is_integer(x) do + 111 + end + defmacrop private_macro_guarded(x) when is_integer(x) do + 111 + end +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/elm.elm b/bins/codefang/tests/fixtures/shotness_languages/elm.elm new file mode 100644 index 00000000..0d0b50fe --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/elm.elm @@ -0,0 +1,4 @@ +module Probe exposing (probe) + +probe x = + x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/fish.fish b/bins/codefang/tests/fixtures/shotness_languages/fish.fish new file mode 100644 index 00000000..5ced317a --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/fish.fish @@ -0,0 +1,3 @@ +function probe + echo 111 +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/fortran.f90 b/bins/codefang/tests/fixtures/shotness_languages/fortran.f90 new file mode 100644 index 00000000..7ec6a4d6 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/fortran.f90 @@ -0,0 +1,4 @@ +function probe(x) result(y) + integer :: x, y + y = x + 111 +end function probe diff --git a/bins/codefang/tests/fixtures/shotness_languages/fortran_program.f90 b/bins/codefang/tests/fixtures/shotness_languages/fortran_program.f90 new file mode 100644 index 00000000..986ffbc2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/fortran_program.f90 @@ -0,0 +1,3 @@ +program probe + print *, 111 +end program probe diff --git a/bins/codefang/tests/fixtures/shotness_languages/fortran_subroutine.f90 b/bins/codefang/tests/fixtures/shotness_languages/fortran_subroutine.f90 new file mode 100644 index 00000000..5effbe1a --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/fortran_subroutine.f90 @@ -0,0 +1,4 @@ +subroutine probe(x) + integer :: x + x = x + 111 +end subroutine probe diff --git a/bins/codefang/tests/fixtures/shotness_languages/go.go b/bins/codefang/tests/fixtures/shotness_languages/go.go new file mode 100644 index 00000000..ee4dd705 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/go.go @@ -0,0 +1,2 @@ +package probe +func probe() int { return 111 } diff --git a/bins/codefang/tests/fixtures/shotness_languages/gotmpl.gotmpl b/bins/codefang/tests/fixtures/shotness_languages/gotmpl.gotmpl new file mode 100644 index 00000000..97c185ae --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/gotmpl.gotmpl @@ -0,0 +1 @@ +{{define "probe"}}111{{end}} diff --git a/bins/codefang/tests/fixtures/shotness_languages/graphql.graphql b/bins/codefang/tests/fixtures/shotness_languages/graphql.graphql new file mode 100644 index 00000000..15264b04 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/graphql.graphql @@ -0,0 +1,3 @@ +query probe { + user { field111 } +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/graphql_anonymous.graphql b/bins/codefang/tests/fixtures/shotness_languages/graphql_anonymous.graphql new file mode 100644 index 00000000..c8459a5e --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/graphql_anonymous.graphql @@ -0,0 +1,3 @@ +{ + field111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/graphql_fragment.graphql b/bins/codefang/tests/fixtures/shotness_languages/graphql_fragment.graphql new file mode 100644 index 00000000..03544d36 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/graphql_fragment.graphql @@ -0,0 +1,3 @@ +fragment probe on User { + field111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/groovy.groovy b/bins/codefang/tests/fixtures/shotness_languages/groovy.groovy new file mode 100644 index 00000000..8b670018 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/groovy.groovy @@ -0,0 +1 @@ +def probe(x) { return x + 111 } diff --git a/bins/codefang/tests/fixtures/shotness_languages/groovy_class.groovy b/bins/codefang/tests/fixtures/shotness_languages/groovy_class.groovy new file mode 100644 index 00000000..738e6559 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/groovy_class.groovy @@ -0,0 +1,5 @@ +class Box { + def probe(x) { + return x + 111 + } +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/haskell.hs b/bins/codefang/tests/fixtures/shotness_languages/haskell.hs new file mode 100644 index 00000000..365daab2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/haskell.hs @@ -0,0 +1 @@ +probe x = x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/java.java b/bins/codefang/tests/fixtures/shotness_languages/java.java new file mode 100644 index 00000000..6c0a3473 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/java.java @@ -0,0 +1 @@ +class Box { int probe() { return 111; } } diff --git a/bins/codefang/tests/fixtures/shotness_languages/javascript.js b/bins/codefang/tests/fixtures/shotness_languages/javascript.js new file mode 100644 index 00000000..ca8fdead --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/javascript.js @@ -0,0 +1 @@ +function probe(x) { return x + 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/kotlin.kt b/bins/codefang/tests/fixtures/shotness_languages/kotlin.kt new file mode 100644 index 00000000..f3241ecd --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/kotlin.kt @@ -0,0 +1 @@ +fun probe(x: Int): Int = x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/latex.tex b/bins/codefang/tests/fixtures/shotness_languages/latex.tex new file mode 100644 index 00000000..88c5d086 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/latex.tex @@ -0,0 +1 @@ +\newcommand{\probe}[1]{#1 + 111} diff --git a/bins/codefang/tests/fixtures/shotness_languages/latex_delimiter.tex b/bins/codefang/tests/fixtures/shotness_languages/latex_delimiter.tex new file mode 100644 index 00000000..fea6de4c --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/latex_delimiter.tex @@ -0,0 +1 @@ +\DeclarePairedDelimiter{\probe}{111}{x} diff --git a/bins/codefang/tests/fixtures/shotness_languages/latex_environment.tex b/bins/codefang/tests/fixtures/shotness_languages/latex_environment.tex new file mode 100644 index 00000000..9578729b --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/latex_environment.tex @@ -0,0 +1 @@ +\newenvironment{probe}{111}{} diff --git a/bins/codefang/tests/fixtures/shotness_languages/latex_old.tex b/bins/codefang/tests/fixtures/shotness_languages/latex_old.tex new file mode 100644 index 00000000..d0d518b2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/latex_old.tex @@ -0,0 +1,3 @@ +\def\probe{ +111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/latex_theorem.tex b/bins/codefang/tests/fixtures/shotness_languages/latex_theorem.tex new file mode 100644 index 00000000..09c32e16 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/latex_theorem.tex @@ -0,0 +1 @@ +\newtheorem{probe}{Probe111} diff --git a/bins/codefang/tests/fixtures/shotness_languages/lua.lua b/bins/codefang/tests/fixtures/shotness_languages/lua.lua new file mode 100644 index 00000000..641f65df --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/lua.lua @@ -0,0 +1,3 @@ +function probe(x) + return x + 111 +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/make.makefile b/bins/codefang/tests/fixtures/shotness_languages/make.makefile new file mode 100644 index 00000000..ce78bb31 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/make.makefile @@ -0,0 +1,2 @@ +probe: + @echo 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/nim.nim b/bins/codefang/tests/fixtures/shotness_languages/nim.nim new file mode 100644 index 00000000..173cbee9 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/nim.nim @@ -0,0 +1,2 @@ +proc probe(x: int): int = + x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/perl.pl b/bins/codefang/tests/fixtures/shotness_languages/perl.pl new file mode 100644 index 00000000..c568b472 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/perl.pl @@ -0,0 +1 @@ +sub probe { return 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/php.php b/bins/codefang/tests/fixtures/shotness_languages/php.php new file mode 100644 index 00000000..0a9d2ac8 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/php.php @@ -0,0 +1,2 @@ + x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/python.py b/bins/codefang/tests/fixtures/shotness_languages/python.py new file mode 100644 index 00000000..936b3506 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/python.py @@ -0,0 +1,2 @@ +def probe(x): + return x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/r.r b/bins/codefang/tests/fixtures/shotness_languages/r.r new file mode 100644 index 00000000..4376387f --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r.r @@ -0,0 +1,6 @@ +probe <- function(x) { + if (x > 0) { + return(111) + } + return(x) +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/r_anonymous.r b/bins/codefang/tests/fixtures/shotness_languages/r_anonymous.r new file mode 100644 index 00000000..7d6ba228 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r_anonymous.r @@ -0,0 +1,3 @@ +lapply(c(1), function(x) { + x + 111 +}) diff --git a/bins/codefang/tests/fixtures/shotness_languages/r_equal.r b/bins/codefang/tests/fixtures/shotness_languages/r_equal.r new file mode 100644 index 00000000..52f064b8 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r_equal.r @@ -0,0 +1,3 @@ +probe = function(x) { + x + 111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/r_right.r b/bins/codefang/tests/fixtures/shotness_languages/r_right.r new file mode 100644 index 00000000..15f6939f --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r_right.r @@ -0,0 +1,3 @@ +(function(x) { + x + 111 +}) -> probe diff --git a/bins/codefang/tests/fixtures/shotness_languages/r_super_left.r b/bins/codefang/tests/fixtures/shotness_languages/r_super_left.r new file mode 100644 index 00000000..27111b66 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r_super_left.r @@ -0,0 +1,3 @@ +probe <<- function(x) { + x + 111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/r_super_right.r b/bins/codefang/tests/fixtures/shotness_languages/r_super_right.r new file mode 100644 index 00000000..73503c47 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/r_super_right.r @@ -0,0 +1,3 @@ +(function(x) { + x + 111 +}) ->> probe diff --git a/bins/codefang/tests/fixtures/shotness_languages/rego.rego b/bins/codefang/tests/fixtures/shotness_languages/rego.rego new file mode 100644 index 00000000..f0002bc1 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/rego.rego @@ -0,0 +1,2 @@ +package example +probe(x) = y { y := x + 111 } diff --git a/bins/codefang/tests/fixtures/shotness_languages/ruby.rb b/bins/codefang/tests/fixtures/shotness_languages/ruby.rb new file mode 100644 index 00000000..30cef7d2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/ruby.rb @@ -0,0 +1,3 @@ +def probe(x) + x + 111 +end diff --git a/bins/codefang/tests/fixtures/shotness_languages/rust.rs b/bins/codefang/tests/fixtures/shotness_languages/rust.rs new file mode 100644 index 00000000..f523124f --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/rust.rs @@ -0,0 +1 @@ +fn probe(x: i32) -> i32 { x + 111 } diff --git a/bins/codefang/tests/fixtures/shotness_languages/scala.scala b/bins/codefang/tests/fixtures/shotness_languages/scala.scala new file mode 100644 index 00000000..a660749b --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/scala.scala @@ -0,0 +1 @@ +def probe(x: Int): Int = x + 111 diff --git a/bins/codefang/tests/fixtures/shotness_languages/sql.sql b/bins/codefang/tests/fixtures/shotness_languages/sql.sql new file mode 100644 index 00000000..b4d64812 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/sql.sql @@ -0,0 +1 @@ +CREATE FUNCTION probe(x integer) RETURNS integer LANGUAGE SQL AS $$ SELECT x + 111 $$; diff --git a/bins/codefang/tests/fixtures/shotness_languages/swift.swift b/bins/codefang/tests/fixtures/shotness_languages/swift.swift new file mode 100644 index 00000000..8f678834 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/swift.swift @@ -0,0 +1 @@ +func probe(_ x: Int) -> Int { return x + 111 } diff --git a/bins/codefang/tests/fixtures/shotness_languages/tcl.tcl b/bins/codefang/tests/fixtures/shotness_languages/tcl.tcl new file mode 100644 index 00000000..647b28a6 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/tcl.tcl @@ -0,0 +1,3 @@ +proc probe {x} { + return 111 +} diff --git a/bins/codefang/tests/fixtures/shotness_languages/tsx.tsx b/bins/codefang/tests/fixtures/shotness_languages/tsx.tsx new file mode 100644 index 00000000..bbc8fdc2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/tsx.tsx @@ -0,0 +1 @@ +function probe(x: number): number { return x + 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/typescript.ts b/bins/codefang/tests/fixtures/shotness_languages/typescript.ts new file mode 100644 index 00000000..bbc8fdc2 --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/typescript.ts @@ -0,0 +1 @@ +function probe(x: number): number { return x + 111; } diff --git a/bins/codefang/tests/fixtures/shotness_languages/zig.zig b/bins/codefang/tests/fixtures/shotness_languages/zig.zig new file mode 100644 index 00000000..0e6fcb9b --- /dev/null +++ b/bins/codefang/tests/fixtures/shotness_languages/zig.zig @@ -0,0 +1 @@ +fn probe(x: i32) i32 { return x + 111; } diff --git a/bins/codefang/tests/shotness_e2e.rs b/bins/codefang/tests/shotness_e2e.rs index 345ef70d..8d3e3781 100644 --- a/bins/codefang/tests/shotness_e2e.rs +++ b/bins/codefang/tests/shotness_e2e.rs @@ -110,6 +110,129 @@ fn commit(repo: &TestRepo, day: i64, files: &[(&str, &[u8])]) { commit_files(repo, &format!("day {day}"), T0 + day * DAY, files).expect("commit"); } +// BUG: specs/bugs/BUG-kotlin-shotness-empty.md +#[test] +fn kotlin_shotness_with_limit_and_text_reports_functions() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[("Calculator.kt", b"fun sum(a: Int, b: Int): Int = a + b\n")], + ); + let text = stdout_ok(&[ + "-a", + SHOTNESS, + "--limit", + "200", + "--format", + "text", + path(&repo), + ]); + assert!(text.contains("1 nodes"), "{text}"); + assert!(text.contains("sum"), "{text}"); +} + +#[test] +fn kotlin_shotness_qualifies_class_methods() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[( + "Calculator.kt", + b"class Calculator { fun sum(a: Int, b: Int): Int = a + b }\n", + )], + ); + assert_eq!( + entities(&shotness(&repo, &[])), + vec![( + "Calculator.sum".into(), + "Function".into(), + "Calculator.kt".into() + )] + ); +} + +#[test] +fn kotlin_shotness_qualifies_object_methods() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[("Service.kt", b"object Service { fun work() = 1 }\n")], + ); + assert_eq!( + entities(&shotness(&repo, &[])), + vec![( + "Service.work".into(), + "Function".into(), + "Service.kt".into() + )] + ); +} + +#[test] +fn kotlin_shotness_qualifies_implicit_companion_methods() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[( + "Service.kt", + b"class Service {\ncompanion object {\nfun work() = 1\n}\n}\n", + )], + ); + assert_eq!( + entities(&shotness(&repo, &[])), + vec![( + "Service.Companion.work".into(), + "Function".into(), + "Service.kt".into() + )] + ); +} + +#[test] +fn kotlin_shotness_qualifies_named_companion_methods() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[( + "Service.kt", + b"class Service {\ncompanion object Factory {\nfun work() = 1\n}\n}\n", + )], + ); + assert_eq!( + entities(&shotness(&repo, &[])), + vec![( + "Service.Factory.work".into(), + "Function".into(), + "Service.kt".into() + )] + ); +} + +#[test] +fn kotlin_script_shotness_counts_body_changes_and_couplings() { + let repo = init_repo().expect("init"); + commit( + &repo, + 0, + &[("script.kts", b"fun first() = 1\nfun second() = 2\n")], + ); + commit( + &repo, + 1, + &[("script.kts", b"fun first() = 10\nfun second() = 2\n")], + ); + let report = shotness(&repo, &["--limit", "200"]); + assert_eq!(total_nodes(&report), 2); + assert_eq!(change_count(&report, "first", "script.kts"), 2); + assert_eq!(change_count(&report, "second", "script.kts"), 1); + assert_eq!(co_changes(&report, "first", "second"), 1); +} + /// `(name, type, file)` of every entity in the report, key-sorted. fn entities(report: &Value) -> Vec<(String, String, String)> { let mut out: Vec<(String, String, String)> = report["node_hotness"] diff --git a/bins/codefang/tests/shotness_languages_e2e.rs b/bins/codefang/tests/shotness_languages_e2e.rs new file mode 100644 index 00000000..49d20101 --- /dev/null +++ b/bins/codefang/tests/shotness_languages_e2e.rs @@ -0,0 +1,247 @@ +//! BUG: specs/bugs/BUG-shotness-language-names.md +//! Named declarations must survive the default Shotness DSL and track body edits. +mod common; + +use cf_gitlib::testutil::{commit_files, init_repo}; +use serde_json::Value; + +const BIN: &str = env!("CARGO_BIN_EXE_codefang"); +const BASE: i64 = 1_609_459_200; +const NEXT: i64 = BASE + 86_400; +const BEFORE: &str = "111"; +const AFTER: &str = "222"; + +fn check(filename: &str, source: &str, expected: &[&str]) { + let repo = init_repo().expect("init fixture"); + commit_files(&repo, "introduce", BASE, &[(filename, source.as_bytes())]) + .expect("commit source"); + let edited = source.replace(BEFORE, AFTER); + assert_ne!(edited, source, "fixture has no body edit: {filename}"); + commit_files(&repo, "edit body", NEXT, &[(filename, edited.as_bytes())]).expect("commit edit"); + let output = common::command(BIN) + .args([ + "run", + "-a", + "history/shotness", + "--limit", + "200", + "--format", + "json", + ]) + .arg(repo.path()) + .output() + .expect("run Shotness"); + assert!( + output.status.success(), + "{filename}: {}", + String::from_utf8_lossy(&output.stderr) + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("JSON report"); + let rows = report["node_hotness"].as_array().expect("entity rows"); + let mut actual: Vec<_> = rows + .iter() + .map(|row| row["name"].as_str().expect("name")) + .collect(); + actual.sort_unstable(); + let mut expected = expected.to_vec(); + expected.sort_unstable(); + assert_eq!(actual, expected, "{filename}: {report}"); + for row in rows { + assert_eq!( + row["change_count"], 2, + "body edit missing in {filename}: {report}" + ); + } + assert_eq!( + report["aggregate"]["total_changes"], + serde_json::json!(expected.len() * 2), + "{filename}: {report}" + ); + let rendered = common::command(BIN) + .args([ + "run", + "-a", + "history/shotness", + "--limit", + "200", + "--format", + "text", + ]) + .env("NO_COLOR", "1") + .arg(repo.path()) + .output() + .expect("render Shotness"); + assert!( + rendered.status.success(), + "{filename}: {}", + String::from_utf8_lossy(&rendered.stderr) + ); + let text = String::from_utf8(rendered.stdout).expect("text report"); + for (label, wanted) in [ + ("Total Nodes", expected.len()), + ("Total Changes", expected.len() * 2), + ] { + let shown = text + .lines() + .find_map(|line| line.trim_start().strip_prefix(label)) + .expect("summary metric"); + assert_eq!(shown.trim(), wanted.to_string(), "{filename}: {text}"); + } +} + +macro_rules! language { + ($test:ident, $filename:literal $(, $expected:literal)*) => { + #[test] + fn $test() { + check( + $filename, + include_str!(concat!("fixtures/shotness_languages/", $filename)), + &[$($expected),*], + ); + } + }; +} +language!(bash, "bash.sh", "probe"); +language!(c, "c.c", "probe"); +language!(c_sharp, "c_sharp.cs", "Box.probe"); +language!(clojure, "clojure.clj", "probe"); +language!(cmake, "cmake.cmake", "probe"); +language!(commonlisp, "commonlisp.lisp", "probe"); +language!(cpp, "cpp.cpp", "probe"); +language!(crystal, "crystal.cr", "probe"); +language!(dart, "dart.dart", "probe"); +language!(elixir, "elixir.ex", "Box.probe"); +language!(elm, "elm.elm", "probe"); +language!(fish, "fish.fish", "probe"); +language!(fortran, "fortran.f90", "probe"); +language!(go, "go.go", "probe"); +language!(gotmpl, "gotmpl.gotmpl", "\"probe\""); +language!(graphql, "graphql.graphql", "probe"); +language!(groovy, "groovy.groovy", "probe"); +language!(haskell, "haskell.hs", "probe"); +language!(java, "java.java", "Box.probe"); +language!(javascript, "javascript.js", "probe"); +language!(kotlin, "kotlin.kt", "probe"); +language!(lua, "lua.lua", "probe"); +language!(make, "make.makefile", "probe"); +language!(nim, "nim.nim", "probe"); +language!(perl, "perl.pl", "probe"); +language!(php, "php.php", "probe"); +language!(powershell, "powershell.ps1", "probe"); +language!(proto, "proto.proto", "Box.probe"); +language!(prql, "prql.prql", "probe"); +language!(r, "r.r", "probe"); +language!(rego, "rego.rego", "probe"); +language!(ruby, "ruby.rb", "probe"); +language!(rust, "rust.rs", "probe"); +language!(scala, "scala.scala", "probe"); +language!(sql, "sql.sql", "probe"); +language!(swift, "swift.swift", "probe"); +language!(tcl, "tcl.tcl", "probe"); +language!(tsx, "tsx.tsx", "probe"); +language!(typescript, "typescript.ts", "probe"); +language!(zig, "zig.zig", "probe"); +language!( + clojure_forms, + "clojure_forms.clj", + "private-probe", + "macro-probe", + "test-probe", + "method-probe" +); +language!( + elixir_forms, + "elixir_forms.ex", + "Box.private_probe", + "Box.macro_probe", + "Box.private_macro", + "Box.zero_probe" +); +language!(cmake_macro, "cmake_macro.cmake", "probe"); +language!( + commonlisp_forms, + "commonlisp_forms.lisp", + "macro-probe", + "generic-probe", + "method-probe" +); +language!(fortran_subroutine, "fortran_subroutine.f90", "probe"); +language!(fortran_program, "fortran_program.f90", "probe"); +language!(graphql_fragment, "graphql_fragment.graphql", "probe"); +language!(graphql_anonymous, "graphql_anonymous.graphql"); +language!(powershell_class, "powershell_class.ps1", "Box.probe"); +language!(groovy_class, "groovy_class.groovy", "Box.probe"); +language!(r_equal, "r_equal.r", "probe"); +language!(r_super_left, "r_super_left.r", "probe"); +language!(r_right, "r_right.r", "probe"); +language!(r_super_right, "r_super_right.r", "probe"); +language!(r_anonymous, "r_anonymous.r"); + +#[test] +fn r_named_function_body_is_measured_by_static_complexity() { + let dir = tempfile::tempdir().expect("tempdir"); + let file = dir.path().join("probe.r"); + std::fs::write(&file, include_str!("fixtures/shotness_languages/r.r")).expect("write fixture"); + let output = common::command(BIN) + .args(["run", "-a", "static/complexity", "--format", "json"]) + .arg(file) + .output() + .expect("run"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("JSON"); + let section = &report["sections"][0]; + assert_eq!(section["metrics"][0]["value"], "1"); + assert_eq!(section["metrics"][2]["value"], "2"); + assert_eq!(section["issues"][0]["name"], "probe"); +} +language!(python, "python.py", "probe"); +language!(css, "css.css"); +language!(latex, "latex.tex", "\\probe"); +language!(latex_environment, "latex_environment.tex", "probe"); +language!(latex_theorem, "latex_theorem.tex", "probe"); +language!(latex_delimiter, "latex_delimiter.tex", "\\probe"); + +#[test] +fn latex_old_definition_is_named_when_introduced() { + let repo = init_repo().expect("fixture"); + let source = include_bytes!("fixtures/shotness_languages/latex_old.tex"); + commit_files(&repo, "introduce definition", BASE, &[("old.tex", source)]).expect("commit"); + let output = common::command(BIN) + .args([ + "run", + "-a", + "history/shotness", + "--limit", + "200", + "--format", + "json", + ]) + .arg(repo.path()) + .output() + .expect("run"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("JSON"); + assert_eq!( + report["node_hotness"].as_array().expect("entities").len(), + 1 + ); + assert_eq!(report["node_hotness"][0]["name"], "\\probe"); + assert_eq!(report["node_hotness"][0]["change_count"], 1); +} + +language!( + elixir_guarded, + "elixir_guarded.ex", + "Box.guarded", + "Box.private_guarded", + "Box.macro_guarded", + "Box.private_macro_guarded" +); diff --git a/bins/codefang/tests/static_walk_e2e.rs b/bins/codefang/tests/static_walk_e2e.rs index d9a9d0f2..4ce8c69b 100644 --- a/bins/codefang/tests/static_walk_e2e.rs +++ b/bins/codefang/tests/static_walk_e2e.rs @@ -309,3 +309,19 @@ fn static_walk_output_is_deterministic() { let yaml_second = run("static/*", "yaml", &[], dir.path()); assert!(yaml_first.1 == yaml_second.1, "yaml runs differ"); } + +// BUG: specs/bugs/BUG-kotlin-empty-analysis.md +#[test] +fn kotlin_sources_and_scripts_produce_function_metrics() { + let dir = tempfile::tempdir().expect("tempdir"); + let source = "class Calculator { companion object { fun sum(a: Int, b: Int): Int = a + b } }\n"; + for path in ["src/main/kotlin/Calculator.kt", "build.gradle.kts"] { + write(dir.path(), path, source); + let report = per_file_json("static/complexity", &[], &dir.path().join(path)); + assert_eq!(report["sections"][0]["metrics"][0]["value"], "1"); + assert_eq!(report["sections"][0]["issues"][0]["name"], "sum"); + } + let report = per_file_json("static/complexity", &[], dir.path()); + assert_eq!(report["sections"][0]["metrics"][0]["value"], "2"); + assert_eq!(file_paths(&report).len(), 2); +} diff --git a/crates/cf-uast-mapping/src/pattern_matcher.rs b/crates/cf-uast-mapping/src/pattern_matcher.rs index ae4e366a..fe91b756 100644 --- a/crates/cf-uast-mapping/src/pattern_matcher.rs +++ b/crates/cf-uast-mapping/src/pattern_matcher.rs @@ -8,7 +8,7 @@ //! a tree-sitter [`Query`] and memoizes it by pattern text, tracking //! hit/miss counters. //! - [`PatternMatcher::match_pattern`] runs a compiled query against a node -//! and returns the captures of the **first** match. +//! and returns the captures of the **first match rooted at that node**. //! //! Cursors are recycled through a small mutex-guarded free list; the cache is //! a mutex-guarded map sharing compiled queries via `Arc`. @@ -101,7 +101,7 @@ impl PatternMatcher { } /// Matches a compiled query against a node and returns the captures of the - /// first match as a name→text map (a [`BTreeMap`] for deterministic + /// first match rooted at that node as a name→text map (a [`BTreeMap`] for deterministic /// ordering). /// /// # Errors @@ -140,6 +140,8 @@ fn match_tree_sitter_query( source: &[u8], ) -> Result, MatchError> { let capture_names = query.capture_names(); + // A declaration's captures must not come from a nested declaration/body. + cursor.set_max_start_depth(Some(0)); let mut matches = cursor.matches(query, node, source); let first = match matches.next() { diff --git a/crates/cf-uast-mappings/src/clojure.rs b/crates/cf-uast-mappings/src/clojure.rs index bfe34733..f60da9ca 100644 --- a/crates/cf-uast-mappings/src/clojure.rs +++ b/crates/cf-uast-mappings/src/clojure.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `clojure.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `clojure` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -96,29 +96,34 @@ pub static CLOJURE: LanguageMapping = uast_language! { // symbol; without these conditioned rules `(defn f [x] ...)` lowered // as a bare Call and the analyzers saw zero functions. Conditioned // rules first; the unconditional `list_lit` below is the catch-all. - list_defn ("(list_lit)") => { + list_defn ("(list_lit . (sym_lit) (sym_lit) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["value == \"defn\""], }, - list_defn_private ("(list_lit)") => { + list_defn_private ("(list_lit . (sym_lit) (sym_lit) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["value == \"defn-\""], }, - list_defmacro ("(list_lit)") => { + list_defmacro ("(list_lit . (sym_lit) (sym_lit) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["value == \"defmacro\""], }, - list_deftest ("(list_lit)") => { + list_deftest ("(list_lit . (sym_lit) (sym_lit) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["value == \"deftest\""], }, - list_defmethod ("(list_lit)") => { + list_defmethod ("(list_lit . (sym_lit) (sym_lit) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["value == \"defmethod\""], }, list_fn ("(list_lit)") => { diff --git a/crates/cf-uast-mappings/src/cmake.rs b/crates/cf-uast-mappings/src/cmake.rs index 27f008d4..97e77a28 100644 --- a/crates/cf-uast-mappings/src/cmake.rs +++ b/crates/cf-uast-mappings/src/cmake.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `cmake.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `cmake` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -145,24 +145,24 @@ pub static CMAKE: LanguageMapping = uast_language! { endwhile_command => { type: Synthetic, }, - function_def => { + function_def ("(function_def (function_command (argument_list . (argument) @declared_name)))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, function_command => { - type: Function, - roles: [Function, Declaration], + type: Synthetic, }, endfunction_command => { type: Synthetic, }, - macro_def => { + macro_def ("(macro_def (macro_command (argument_list . (argument) @declared_name)))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, macro_command => { - type: Function, - roles: [Function, Declaration], + type: Synthetic, }, endmacro_command => { type: Synthetic, diff --git a/crates/cf-uast-mappings/src/commonlisp.rs b/crates/cf-uast-mappings/src/commonlisp.rs index 9aa26270..b1572380 100644 --- a/crates/cf-uast-mappings/src/commonlisp.rs +++ b/crates/cf-uast-mappings/src/commonlisp.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `commonlisp.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `commonlisp` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -73,30 +73,16 @@ pub static COMMONLISP: LanguageMapping = uast_language! { type: Dict, token: self, }, - defun => { + // The grammar also aliases defmacro/defgeneric/defmethod to defun. + defun ("(defun (defun_header function_name: (sym_lit) @declared_name))") => { type: Function, token: self, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, defun_header => { - type: Function, - token: self, - roles: [Function, Declaration], - }, - defmacro => { - type: Function, - token: self, - roles: [Function, Declaration], - }, - defmethod => { - type: Method, - token: self, - roles: [Function, Declaration, Member], - }, - defgeneric => { - type: Function, + type: Synthetic, token: self, - roles: [Function, Declaration], }, package_lit => { type: Module, diff --git a/crates/cf-uast-mappings/src/elixir.rs b/crates/cf-uast-mappings/src/elixir.rs index 9a8cd698..4286ccb6 100644 --- a/crates/cf-uast-mappings/src/elixir.rs +++ b/crates/cf-uast-mappings/src/elixir.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `elixir.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `elixir` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -128,29 +128,34 @@ pub static ELIXIR: LanguageMapping = uast_language! { // lowered as a bare Call and the analyzers saw zero functions. // Declaration order matters: conditioned rules first, the // unconditional `call` rule below is the catch-all. - call_def ("(call)") => { + call_def ("(call (arguments [(call target: (identifier) @declared_name) (identifier) @declared_name (binary_operator left: (call target: (identifier) @declared_name))]))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["target == \"def\""], }, - call_defp ("(call)") => { + call_defp ("(call (arguments [(call target: (identifier) @declared_name) (identifier) @declared_name (binary_operator left: (call target: (identifier) @declared_name))]))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["target == \"defp\""], }, - call_defmacro ("(call)") => { + call_defmacro ("(call (arguments [(call target: (identifier) @declared_name) (identifier) @declared_name (binary_operator left: (call target: (identifier) @declared_name))]))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["target == \"defmacro\""], }, - call_defmacrop ("(call)") => { + call_defmacrop ("(call (arguments [(call target: (identifier) @declared_name) (identifier) @declared_name (binary_operator left: (call target: (identifier) @declared_name))]))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, when: ["target == \"defmacrop\""], }, - call_defmodule ("(call)") => { + call_defmodule ("(call (arguments (alias) @declared_name))") => { type: Class, roles: [Class, Declaration], + props: { "name": "@declared_name" }, when: ["target == \"defmodule\""], }, call => { diff --git a/crates/cf-uast-mappings/src/elm.rs b/crates/cf-uast-mappings/src/elm.rs index ba775aea..06a4473c 100644 --- a/crates/cf-uast-mappings/src/elm.rs +++ b/crates/cf-uast-mappings/src/elm.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `elm.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `elm` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -66,15 +66,19 @@ pub static ELM: LanguageMapping = uast_language! { token: self, roles: [Literal], }, + named_function ("(value_declaration (function_declaration_left . (lower_case_identifier) @declared_name))") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["functionDeclarationLeft.type == \"function_declaration_left\""], + }, value_declaration => { type: Variable, token: self, roles: [Variable, Declaration], }, function_declaration_left => { - type: Function, + type: Synthetic, token: self, - roles: [Function, Declaration], }, anonymous_function_expr => { type: Lambda, diff --git a/crates/cf-uast-mappings/src/fortran.rs b/crates/cf-uast-mappings/src/fortran.rs index 7fc1e74e..e968b9ea 100644 --- a/crates/cf-uast-mappings/src/fortran.rs +++ b/crates/cf-uast-mappings/src/fortran.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `fortran.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `fortran` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -15,10 +15,11 @@ pub static FORTRAN: LanguageMapping = uast_language! { // executable body with its own control flow and complexity. Mapped as // File it was invisible to function detection, leaving the many // program-only example/driver files at zero functions. - program => { + program ("(program (program_statement (name) @declared_name))") => { type: Function, token: self, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, program_statement => { type: Module, @@ -110,25 +111,25 @@ pub static FORTRAN: LanguageMapping = uast_language! { token: self, roles: [Module], }, - function => { + function ("(function (function_statement name: (name) @declared_name))") => { type: Function, token: self, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, function_statement => { - type: Function, + type: Synthetic, token: self, - roles: [Function, Declaration], }, - subroutine => { + subroutine ("(subroutine (subroutine_statement name: (name) @declared_name))") => { type: Function, token: self, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, subroutine_statement => { - type: Function, + type: Synthetic, token: self, - roles: [Function, Declaration], }, interface => { type: Interface, diff --git a/crates/cf-uast-mappings/src/graphql.rs b/crates/cf-uast-mappings/src/graphql.rs index 10f809b1..317be84b 100644 --- a/crates/cf-uast-mappings/src/graphql.rs +++ b/crates/cf-uast-mappings/src/graphql.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `graphql.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `graphql` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -59,18 +59,20 @@ pub static GRAPHQL: LanguageMapping = uast_language! { executable_definition => { type: Synthetic, }, - operation_definition => { + operation_definition ("(operation_definition (name) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, operation_type => { type: Identifier, token: self, roles: [Name], }, - fragment_definition => { + fragment_definition ("(fragment_definition (fragment_name) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, fragment_name => { type: Identifier, diff --git a/crates/cf-uast-mappings/src/groovy.rs b/crates/cf-uast-mappings/src/groovy.rs index 343af679..9cad41bd 100644 --- a/crates/cf-uast-mappings/src/groovy.rs +++ b/crates/cf-uast-mappings/src/groovy.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `groovy.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `groovy` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -39,6 +39,7 @@ pub static GROOVY: LanguageMapping = uast_language! { function_definition => { type: Function, roles: [Function, Declaration], + props: { "name": "@function" }, }, identifier => { type: Identifier, diff --git a/crates/cf-uast-mappings/src/kotlin.rs b/crates/cf-uast-mappings/src/kotlin.rs index 8bf86f38..5949d905 100644 --- a/crates/cf-uast-mappings/src/kotlin.rs +++ b/crates/cf-uast-mappings/src/kotlin.rs @@ -216,9 +216,12 @@ pub static KOTLIN: LanguageMapping = uast_language! { finally => { type: Synthetic, }, - function_declaration => { + // Kotlin has no `name` field: capture the direct declaration child, + // excluding identifiers inside annotations, receivers and bodies. + function_declaration ("(function_declaration (simple_identifier) @name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@name" }, }, anonymous_function => { type: Function, @@ -267,9 +270,10 @@ pub static KOTLIN: LanguageMapping = uast_language! { type_parameters => { type: Synthetic, }, - class_declaration => { + class_declaration ("(class_declaration (type_identifier) @name)") => { type: Class, roles: [Class, Declaration], + props: { "name": "@name" }, }, class_body => { type: Block, @@ -282,13 +286,21 @@ pub static KOTLIN: LanguageMapping = uast_language! { class => { type: Synthetic, }, - object_declaration => { + object_declaration ("(object_declaration (type_identifier) @name)") => { type: Class, roles: [Class, Declaration], + props: { "name": "@name" }, + }, + named_companion_object ("(companion_object (type_identifier) @name)") => { + type: Class, + roles: [Class, Static], + props: { "name": "@name" }, + when: ["type_identifier != \"\""], }, companion_object => { type: Class, roles: [Class, Static], + props: { "name": "Companion" }, }, object => { type: Synthetic, diff --git a/crates/cf-uast-mappings/src/latex.rs b/crates/cf-uast-mappings/src/latex.rs index ec45992d..40e07bc5 100644 --- a/crates/cf-uast-mappings/src/latex.rs +++ b/crates/cf-uast-mappings/src/latex.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `latex.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `latex` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. //! //! Plain-literal form (the `uast_language!` escape hatch); rules forcing it: //! * `_` — rule name is not a macro identifier. @@ -378,35 +378,35 @@ pub static LATEX: LanguageMapping = LanguageMapping { }, MappingRule { name: "new_command_definition", - pattern: "(new_command_definition)", + pattern: "(new_command_definition declaration: [(curly_group_command_name command: (command_name) @declared_name) (command_name) @declared_name])", extends: "", uast_type: UastType::Function, token: TokenSource::None, roles: &[Role::Function, Role::Declaration], children: &[], - props: &[], + props: &[("name", "@declared_name")], conditions: &[], }, MappingRule { name: "environment_definition", - pattern: "(environment_definition)", + pattern: "(environment_definition name: (curly_group_text text: (text) @declared_name))", extends: "", uast_type: UastType::Function, token: TokenSource::SelfText, roles: &[Role::Function, Role::Declaration], children: &[], - props: &[], + props: &[("name", "@declared_name")], conditions: &[], }, MappingRule { name: "old_command_definition", - pattern: "(old_command_definition)", + pattern: "(old_command_definition declaration: (command_name) @declared_name)", extends: "", uast_type: UastType::Function, token: TokenSource::SelfText, roles: &[Role::Function, Role::Declaration], children: &[], - props: &[], + props: &[("name", "@declared_name")], conditions: &[], }, MappingRule { @@ -928,13 +928,13 @@ pub static LATEX: LanguageMapping = LanguageMapping { }, MappingRule { name: "theorem_definition", - pattern: "(theorem_definition)", + pattern: "(theorem_definition name: (curly_group_text_list text: (text) @declared_name))", extends: "", uast_type: UastType::Function, token: TokenSource::None, roles: &[Role::Function, Role::Declaration], children: &[], - props: &[], + props: &[("name", "@declared_name")], conditions: &[], }, MappingRule { @@ -1071,13 +1071,13 @@ pub static LATEX: LanguageMapping = LanguageMapping { }, MappingRule { name: "paired_delimiter_definition", - pattern: "(paired_delimiter_definition)", + pattern: "(paired_delimiter_definition declaration: (curly_group_command_name command: (command_name) @declared_name))", extends: "", uast_type: UastType::Function, token: TokenSource::None, roles: &[Role::Function, Role::Declaration], children: &[], - props: &[], + props: &[("name", "@declared_name")], conditions: &[], }, MappingRule { diff --git a/crates/cf-uast-mappings/src/make.rs b/crates/cf-uast-mappings/src/make.rs index 6d00b751..6bca6cd1 100644 --- a/crates/cf-uast-mappings/src/make.rs +++ b/crates/cf-uast-mappings/src/make.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `make.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `make` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -40,6 +40,7 @@ pub static MAKE: LanguageMapping = uast_language! { rule => { type: Function, roles: [Function, Declaration], + props: { "name": "descendant:targets" }, }, targets => { type: Identifier, diff --git a/crates/cf-uast-mappings/src/powershell.rs b/crates/cf-uast-mappings/src/powershell.rs index 2157a77b..4d362b91 100644 --- a/crates/cf-uast-mappings/src/powershell.rs +++ b/crates/cf-uast-mappings/src/powershell.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `powershell.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `powershell` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -27,9 +27,10 @@ pub static POWERSHELL: LanguageMapping = uast_language! { type: Identifier, roles: [Argument], }, - function_statement => { + function_statement ("(function_statement (function_name) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, function_name => { type: Identifier, @@ -51,17 +52,19 @@ pub static POWERSHELL: LanguageMapping = uast_language! { type: Literal, roles: [Literal], }, - class_statement => { + class_statement ("(class_statement (simple_name) @declared_name)") => { type: Class, roles: [Declaration], + props: { "name": "@declared_name" }, }, class_property_definition => { type: Variable, roles: [Variable, Declaration], }, - class_method_definition => { + class_method_definition ("(class_method_definition (simple_name) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, class_method_parameter => { type: Parameter, diff --git a/crates/cf-uast-mappings/src/proto.rs b/crates/cf-uast-mappings/src/proto.rs index bb1f2f3b..1244d084 100644 --- a/crates/cf-uast-mappings/src/proto.rs +++ b/crates/cf-uast-mappings/src/proto.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `proto.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `proto` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -99,6 +99,7 @@ pub static PROTO: LanguageMapping = uast_language! { service => { type: Interface, roles: [Interface, Declaration], + props: { "name": "descendant:service_name" }, }, service_name => { type: Identifier, @@ -107,7 +108,8 @@ pub static PROTO: LanguageMapping = uast_language! { }, rpc => { type: Method, - roles: [Method, Declaration], + roles: [Method, Function, Declaration], + props: { "name": "descendant:rpc_name" }, }, rpc_name => { type: Identifier, diff --git a/crates/cf-uast-mappings/src/r.rs b/crates/cf-uast-mappings/src/r.rs index 60c1db79..897df492 100644 --- a/crates/cf-uast-mappings/src/r.rs +++ b/crates/cf-uast-mappings/src/r.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `r.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `r` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -23,9 +23,31 @@ pub static R: LanguageMapping = uast_language! { type: Identifier, roles: [Argument], }, + // Named assignments own the Function identity and full span. Their + // literal body is structural; other function values remain Lambdas. + assigned_function_body ("(function_definition)") => { + type: Synthetic, + when: ["parent.operator == \"<-\"", "parent.rhs.type == \"function_definition\""], + }, + assigned_body_equal ("(function_definition)") => { + type: Synthetic, + when: ["parent.operator == \"=\"", "parent.rhs.type == \"function_definition\""], + }, + assigned_body_super_left ("(function_definition)") => { + type: Synthetic, + when: ["parent.operator == \"<<-\"", "parent.rhs.type == \"function_definition\""], + }, + assigned_body_right ("(function_definition)") => { + type: Synthetic, + when: ["parent.type == \"parenthesized_expression\"", "parent.parent.operator == \"->\"", "parent.parent.lhs.body.type == \"function_definition\""], + }, + assigned_body_super_right ("(function_definition)") => { + type: Synthetic, + when: ["parent.type == \"parenthesized_expression\"", "parent.parent.operator == \"->>\"", "parent.parent.lhs.body.type == \"function_definition\""], + }, function_definition => { - type: Function, - roles: [Function, Declaration], + type: Lambda, + roles: [Lambda], }, parameters => { type: Block, @@ -67,6 +89,31 @@ pub static R: LanguageMapping = uast_language! { type: Return, roles: [Return], }, + assigned_function ("(binary_operator lhs: (_) @declared_name)") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["operator == \"<-\"", "rhs.type == \"function_definition\""], + }, + assigned_equal ("(binary_operator lhs: (_) @declared_name)") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["operator == \"=\"", "rhs.type == \"function_definition\""], + }, + assigned_super_left ("(binary_operator lhs: (_) @declared_name)") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["operator == \"<<-\"", "rhs.type == \"function_definition\""], + }, + assigned_right ("(binary_operator rhs: (_) @declared_name)") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["operator == \"->\"", "lhs.body.type == \"function_definition\""], + }, + assigned_super_right ("(binary_operator rhs: (_) @declared_name)") => { + type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, + when: ["operator == \"->>\"", "lhs.body.type == \"function_definition\""], + }, binary_operator => { type: BinaryOp, roles: [Operator], diff --git a/crates/cf-uast-mappings/src/rego.rs b/crates/cf-uast-mappings/src/rego.rs index 3500e3c1..59cad537 100644 --- a/crates/cf-uast-mappings/src/rego.rs +++ b/crates/cf-uast-mappings/src/rego.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `rego.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `rego` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -48,9 +48,10 @@ pub static REGO: LanguageMapping = uast_language! { type: Index, roles: [Index], }, - rule => { + rule ("(rule (rule_head . (var) @declared_name))") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, rule_head => { type: Identifier, diff --git a/crates/cf-uast-mappings/src/sql.rs b/crates/cf-uast-mappings/src/sql.rs index 0ec3751a..57dbc1d2 100644 --- a/crates/cf-uast-mappings/src/sql.rs +++ b/crates/cf-uast-mappings/src/sql.rs @@ -1,5 +1,5 @@ -//! GENERATED by uastmap2rs — transpiled from `sql.uastmap`. Do not edit by -//! hand; re-run the transpiler and commit its output instead. +//! Mapping table for `sql` — hand-maintained (the system of record). +//! Keep grammar-conformance, shape and Shotness language tests green when editing. use cf_uast_mapping::{uast_language, LanguageMapping}; @@ -34,9 +34,10 @@ pub static SQL: LanguageMapping = uast_language! { // `CREATE FUNCTION` bodies were previously unmapped, so SQL files // defining functions reported zero for the complexity/cohesion // analyzers. - create_function => { + create_function ("(create_function (object_reference) @declared_name)") => { type: Function, roles: [Function, Declaration], + props: { "name": "@declared_name" }, }, function_body => { type: Block, diff --git a/crates/cf-uast-mappings/tests/grammar_conformance.rs b/crates/cf-uast-mappings/tests/grammar_conformance.rs index fa149215..6682afe8 100644 --- a/crates/cf-uast-mappings/tests/grammar_conformance.rs +++ b/crates/cf-uast-mappings/tests/grammar_conformance.rs @@ -732,9 +732,6 @@ const DEAD_RULE_ALLOW_LIST: &[(&str, &str)] = &[ // compiled at runtime either — the lowering stores query None and // falls through, which is the frozen behavior). Regenerate with the // conformance test output when wiring more grammars. - ("commonlisp", "defgeneric"), - ("commonlisp", "defmacro"), - ("commonlisp", "defmethod"), ("commonlisp", "finally"), ("commonlisp", "return"), ("crystal", "abstract"), diff --git a/crates/cf-uast-mappings/tests/shape_contract.rs b/crates/cf-uast-mappings/tests/shape_contract.rs index 1b8c880d..d9211526 100644 --- a/crates/cf-uast-mappings/tests/shape_contract.rs +++ b/crates/cf-uast-mappings/tests/shape_contract.rs @@ -41,6 +41,25 @@ fn names(nodes: &[&Node]) -> Vec { .collect() } +// BUG: specs/bugs/BUG-kotlin-shotness-empty.md +#[test] +fn kotlin_names_exclude_annotation_receiver_and_body_identifiers() { + let root = parse( + "Names.kt", + "@Suppress(\"unused\")\nfun List.head(): T = first()\nfun regular() = 1\n", + ); + assert_eq!(names(&of_type(&root, "Function")), ["head", "regular"]); +} + +#[test] +fn kotlin_implicit_companion_does_not_take_a_nested_companion_name() { + let root = parse("Nested.kt", "class Outer {\ncompanion object {\nclass Inner {\ncompanion object Factory {\nfun work() = 1\n}\n}\n}\n}\n"); + assert_eq!( + names(&of_type(&root, "Class")), + ["Outer", "Companion", "Inner", "Factory"] + ); +} + fn operator(n: &Node) -> &str { n.props.get(PROP_OPERATOR).map_or("", String::as_str) } diff --git a/crates/cf-uast-mappings/tests/shotness_fixtures.rs b/crates/cf-uast-mappings/tests/shotness_fixtures.rs new file mode 100644 index 00000000..efc38021 --- /dev/null +++ b/crates/cf-uast-mappings/tests/shotness_fixtures.rs @@ -0,0 +1,84 @@ +//! BUG: specs/bugs/BUG-shotness-language-names.md +//! Ensure the CLI language matrix uses valid syntax for the pinned grammars. + +#[test] +fn shotness_language_fixtures_have_no_parse_errors() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../bins/codefang/tests/fixtures/shotness_languages"); + let parser = cf_uast::Parser::new(); + for entry in std::fs::read_dir(root).expect("fixture directory") { + let path = entry.expect("fixture entry").path(); + let filename = path.file_name().expect("filename").to_str().expect("UTF-8"); + let language = parser.get_language(filename); + let grammar = cf_uast::languages::get_language(&language).expect("wired grammar"); + let mut raw = tree_sitter::Parser::new(); + raw.set_language(&grammar).expect("set grammar"); + let source = std::fs::read(&path).expect("read fixture"); + let tree = raw.parse(source, None).expect("parse fixture"); + assert!( + !tree.root_node().has_error(), + "{filename}: {}", + tree.root_node().to_sexp() + ); + } +} + +#[test] +fn latex_old_definition_exposes_its_declared_name() { + // The pinned grammar models only the \def header, not its implementation. + let source = + include_bytes!("../../../bins/codefang/tests/fixtures/shotness_languages/latex_old.tex"); + let root = cf_uast::Parser::new() + .parse("old.tex", source) + .expect("parse"); + let functions = root.find(|node| node.node_type == "Function"); + assert_eq!(functions.len(), 1); + assert_eq!( + functions[0].props.get("name").map(String::as_str), + Some("\\probe") + ); +} + +#[test] +fn every_filename_selected_function_mapping_has_a_history_fixture() { + let parser = cf_uast::Parser::new(); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../bins/codefang/tests/fixtures/shotness_languages"); + let covered: std::collections::BTreeSet<_> = std::fs::read_dir(root) + .expect("fixtures") + .map(|entry| { + parser.get_language(entry.expect("entry").file_name().to_str().expect("UTF-8")) + }) + .collect(); + assert_eq!( + parser.get_language("chart.yaml"), + "yaml", + "review Helm routing exclusion" + ); + assert!( + cf_uast_mappings::by_name("rust_with_rstml") + .expect("RSTML mapping") + .extensions + .is_empty(), + "review RSTML routing exclusion" + ); + for (name, mapping) in cf_uast_mappings::ALL { + if matches!(*name, "helm" | "rust_with_rstml") { + continue; + } + let (rules, _) = mapping.to_rules(); + let declares_functions = rules.iter().any(|rule| { + matches!(rule.uast_spec.r#type.as_str(), "Function" | "Method") + || (rule.uast_spec.roles.iter().any(|role| role == "Function") + && rule + .uast_spec + .roles + .iter() + .any(|role| role == "Declaration")) + }); + assert!( + !declares_functions || covered.contains(*name), + "missing Shotness fixture: {name}" + ); + } +} diff --git a/crates/cf-uast/src/lowering.rs b/crates/cf-uast/src/lowering.rs index 772c9651..011e0b34 100644 --- a/crates/cf-uast/src/lowering.rs +++ b/crates/cf-uast/src/lowering.rs @@ -735,8 +735,11 @@ impl<'a> Lowering<'a> { rule.uast_spec.roles.clone() } - /// Fills `props["name"]` from the node's `name` field, when present. + /// Falls back to the grammar's `name` field after explicit mapping props. fn extract_name(&self, root: TsNode<'_>, _rule: &Rule, props: &mut HashMap) { + if props.contains_key("name") { + return; + } let name = self.extract_name_from_field(root, "name"); if !name.is_empty() { props.insert("name".to_string(), name); diff --git a/crates/cf-uast/tests/lowering_contract.rs b/crates/cf-uast/tests/lowering_contract.rs index 52e6d33a..e9519cd6 100644 --- a/crates/cf-uast/tests/lowering_contract.rs +++ b/crates/cf-uast/tests/lowering_contract.rs @@ -176,3 +176,22 @@ fn parent_conditions_select_rules_by_enclosing_node() { assert_eq!(of_type(&root, "Lambda").len(), 1); assert_eq!(of_type(&root, "Function").len(), 1); } + +// BUG: specs/bugs/BUG-shotness-language-names.md +#[test] +fn mapping_capture_matches_the_current_node_before_nested_expressions() { + let lang = cf_uast::languages::get_language("r").expect("R grammar"); + let mut parser = tree_sitter::Parser::new(); + parser.set_language(&lang).expect("set language"); + let source = b"(function(x) { x + 111 }) -> probe\n"; + let tree = parser.parse(source, None).expect("parse"); + let root = tree.root_node().named_child(0).expect("assignment"); + let matcher = cf_uast_mapping::PatternMatcher::new(lang); + let query = matcher + .compile_and_cache("(binary_operator rhs: (_) @name)") + .expect("query"); + assert_eq!( + matcher.match_pattern(&query, root, source).expect("match")["name"], + "probe" + ); +} diff --git a/docs/kotlin-analysis.md b/docs/kotlin-analysis.md new file mode 100644 index 00000000..189fb82b --- /dev/null +++ b/docs/kotlin-analysis.md @@ -0,0 +1,56 @@ +# Troubleshooting empty Kotlin analysis + +Kotlin `.kt` sources and `.kts` scripts are supported. Check a source file +containing a function directly: + +```sh +uast parse path/to/Calculator.kt --format json +codefang run -a static/complexity --format json --per-file /path/to/repo +``` + +If `uast` reports `no tree-sitter grammar wired for language kotlin`, rebuild +both tools from the current checkout: + +```sh +make install +command -v codefang +command -v uast +``` + +Ensure the resolved paths point to the newly installed tools. A reproduced case +with an older installed build reported zero functions for Spring Petclinic +Kotlin; the current checkout reported 96. See the +[investigation](../specs/bugs/BUG-kotlin-empty-analysis.md) for pinned inputs. + +For files intentionally excluded by generated or vendor rules, inspect the +applicable flags using `codefang run --help`. Those filters are separate from +grammar availability. + +The published [installation guide](../site/getting-started/installation.md) +also includes this diagnostic. + +## Empty Shotness history + +Test the history analyzer separately from static complexity: + +```sh +codefang run -a history/shotness --limit 200 --format text /path/to/repo +``` + +An older Kotlin mapping can parse functions successfully while omitting +`props.name`, which the default Shotness query needs. This produces zero nodes +even when static complexity finds functions. The mapping now captures Kotlin +function, class and object names, including named and implicit companion +objects. For example, methods are tracked as `Service.work`, +`Service.Companion.work` or `Service.Factory.work`. + +Rebuild both tools using `make install` after updating the source. The same +200-commit command on Spring Petclinic Kotlin changed from zero nodes to +14 nodes, 21 changes and 29 couplings after this fix. See the +[Shotness investigation](../specs/bugs/BUG-kotlin-shotness-empty.md). + +For containerized runs, rebuild and redeploy the image from the updated source +so the container includes the corrected Kotlin mapping. + +The [cross-language audit](shotness-languages.md) records the related fixes and +coverage for other language mappings. diff --git a/docs/shotness-languages.md b/docs/shotness-languages.md new file mode 100644 index 00000000..f9366dc0 --- /dev/null +++ b/docs/shotness-languages.md @@ -0,0 +1,61 @@ +# Shotness language coverage + +Shotness needs a declaration name and a span covering the declaration's body. +A successful parse or a nonempty static complexity report does not establish +that history analysis can identify the same declaration. + +Run the history path directly: + +```sh +codefang run -a history/shotness --limit 200 --format text /path/to/repo +``` + +Use `--format json` to inspect `node_hotness` names and change counts. An empty +commit window or changes limited to function-free files can legitimately +produce zero entities. + +## Verified mappings + +The regression matrix covers representative named declarations in all 42 +filename-selected mappings that declare functions, methods or structural entry +points. It uses real Git fixtures and both text and JSON output. Body-edit +fixtures introduce declarations, then change their bodies in a second commit; +every named entity must have the correct name and two touches. + +| Result | Languages | +|---|---| +| Naming, selection or span gaps repaired in this audit | Clojure, CMake, Common Lisp, Elixir, Elm, Fortran, GraphQL, Groovy, LaTeX, Make, PowerShell, Protocol Buffers, R, Rego, SQL | +| Representative cases passed before these repairs | Bash, C, C++, C#, Crystal, Dart, Fish, Go, Go templates, Haskell, Java, JavaScript, Kotlin, Lua, Nim, Perl, PHP, PRQL, Python, Ruby, Rust, Scala, Swift, Tcl, TypeScript, TSX, Zig | + +Additional cases cover private definitions, macros, Elixir guards, class +qualification, anonymous values and all five R assignment operators. A separate +check rejects fixtures with parser errors. A registry-coverage test requires a +history fixture when another function-declaring mapping becomes selectable. + +The shared matcher now roots captures at the current CST node, so an identifier +or literal inside a nested expression cannot become its parent's name. Explicit +mapping names take precedence over the grammar's raw `name` field, allowing +wrapped names such as LaTeX environment names to be normalized. + +## Limits + +CSS function references are tested as references, with zero declaration entities. +The other mappings without function declarations likewise have no default +Shotness entities. Helm's overlapping YAML extensions currently select YAML; +the `rust_with_rstml` mapping has no filename extension. Those two registry +selection limits are asserted separately from name extraction. +The matrix uses registered extensions; conventional names such as `Makefile` +and `CMakeLists.txt` are outside this parser's extension-based selection. + +The pinned LaTeX grammar exposes only the header of an old-style `\def` command. +Its name and introduction are tracked; body-only edits on later lines cannot +be attributed to that header. New-style commands, environments, theorems +and paired delimiters have body-edit coverage. These fixtures do not establish +coverage of every dialect, overload or multi-clause definition. Rust `impl` +method qualification retains the limit described in the +[Shotness reference](../site/analyzers/shotness.md). + +After updating the source, rebuild the binaries with `make install`. For a +containerized deployment, rebuild and redeploy the image. See the +[investigation](../specs/bugs/BUG-shotness-language-names.md) and +[CLI regression matrix](../bins/codefang/tests/shotness_languages_e2e.rs). diff --git a/site/analyzers/shotness.md b/site/analyzers/shotness.md index 3dc1531b..5db530bd 100644 --- a/site/analyzers/shotness.md +++ b/site/analyzers/shotness.md @@ -32,12 +32,31 @@ receiver type of a method declared as `func (a *A) String()` (`A.String`). Two `String()` methods on two receivers are therefore two entities. A node whose name DSL yields nothing (an anonymous lambda) is not an entity. +Kotlin `.kt` sources and `.kts` scripts expose names for functions, classes and +objects. Companion methods include the companion name in their qualification: +`Service.Companion.work` for an implicit companion and `Service.Factory.work` +for `companion object Factory`. Older Kotlin mappings omitted `props.name`, +which made the default Shotness query yield zero nodes even when static +complexity found functions. Rebuild from the updated source if affected. + Rust `impl` methods are qualified only by their enclosing module-level declarations: the mapping does not expose the `impl` target, so `fn m` in -`impl A` is the entity `m`. Mappings that set no `props.name` on -declarations (the real-world set in the -[shape contract](../architecture/uast.md#shape-contract) does) yield no -entities for that language. +`impl A` is the entity `m`. A selected declaration whose name query yields +nothing is excluded. + +The cross-language regression matrix exercises representative named declarations +in all 42 filename-selected mappings that declare functions, methods or structural +entry points. It verifies introduction and body edits through the real history +CLI, with text and JSON output, and includes private definitions, macros, guards +and anonymous-value exclusions. A registry test requires a fixture when another +function-declaring mapping becomes selectable. CSS function references have +zero declaration entities. Helm YAML currently selects the YAML mapping; +`rust_with_rstml` has no filename extension. + +LaTeX new-style commands, environments, theorems and paired delimiters have +body-edit coverage. The pinned grammar exposes only the header of old-style +`\def` commands: their names and introduction are tracked, while body-only edits +on later lines are not attributable to the header. ## Entity lifecycle diff --git a/site/architecture/uast.md b/site/architecture/uast.md index 72e825ea..5cce6900 100644 --- a/site/architecture/uast.md +++ b/site/architecture/uast.md @@ -271,6 +271,10 @@ of relying on either. ### Names +Mapping queries match at the CST node being lowered. Descendant captures +explicitly included in the pattern are allowed; an independent match inside +a body cannot supply the current declaration's name. + Every `Function`, `Method`, `Class`, `Struct`, `Interface` and `Enum` declaration carries `props.name`, and there is exactly **one** such node per declaration: bodies are `Block`s, declarators and parameter-type lists are diff --git a/site/getting-started/installation.md b/site/getting-started/installation.md index 7cc28589..bffe703c 100644 --- a/site/getting-started/installation.md +++ b/site/getting-started/installation.md @@ -11,7 +11,7 @@ Before you begin, make sure the following tools are available on your system: | Requirement | Minimum Version | Notes | |-------------|-----------------|-------| -| **Rust** | stable (recent) | The project is a Cargo workspace under `rust/` | +| **Rust** | pinned by `rust-toolchain.toml` | The repository root is a Cargo workspace | | **Git** | 2.x | Used to clone (with submodules) and by history analyzers | | **C compiler** | GCC or Clang | Builds the vendored libgit2 via the `git2` crate | | **CMake** | 3.14+ | Needed by the `git2` crate's build of vendored libgit2 | @@ -32,7 +32,7 @@ both binaries with Cargo: ```bash git clone --recurse-submodules https://github.com/Sumatoshi-tech/codefang.git -cd codefang/rust +cd codefang cargo build --release -p codefang -p uast ``` @@ -93,6 +93,27 @@ After building, confirm both binaries work: $ uast version ``` +### Empty Kotlin analysis + +If a Kotlin repository reports zero functions, check a source file directly: + +```bash +uast parse path/to/Calculator.kt --format json +``` + +An error containing `no tree-sitter grammar wired for language kotlin` identifies +an older parser build without the Kotlin grammar. From the current checkout, +install both tools and verify the executables selected by your shell: + +```bash +make install +command -v codefang +command -v uast +codefang run -a static/complexity --format json --per-file /path/to/repo +``` + +Both `.kt` sources and `.kts` scripts are supported by the current build. + --- ## Platform notes diff --git a/specs/bugs/BUG-kotlin-empty-analysis.md b/specs/bugs/BUG-kotlin-empty-analysis.md new file mode 100644 index 00000000..63d64866 --- /dev/null +++ b/specs/bugs/BUG-kotlin-empty-analysis.md @@ -0,0 +1,88 @@ +# BUG-kotlin-empty-analysis: installed tools lack the Kotlin grammar + +## Summary + +The installed tools return empty Kotlin static complexity because their parser +build does not include the Kotlin grammar; the current checkout already includes it. + +This investigation covered `static/complexity`. The screenshot's exact +`history/shotness --limit 200 --format text` command exposed an additional bug in +the current source: missing declaration names. See +[BUG-kotlin-shotness-empty](BUG-kotlin-shotness-empty.md) for its reproduction +and mapping fix. Passing static complexity did not establish Shotness correctness. + +## Reproduction + +- Method: manual, using the real installed binaries and a public repository. +- Repository: [Spring Petclinic Kotlin](https://github.com/spring-petclinic/spring-petclinic-kotlin/commit/817dba3b02261e4291f138d5d8bb8c85c7d49758). +- Download: `curl -fL https://codeload.github.com/spring-petclinic/spring-petclinic-kotlin/tar.gz/817dba3b02261e4291f138d5d8bb8c85c7d49758 -o /tmp/petclinic.tar.gz` +- Extract: `tar -xzf /tmp/petclinic.tar.gz -C /tmp` +- Command: `~/.cargo/bin/codefang run -a static/complexity --format json --per-file /tmp/spring-petclinic-kotlin-817dba3b02261e4291f138d5d8bb8c85c7d49758` +- Evidence: `Total Functions` is `0` in [installed-before.json](../runs/kotlin-empty-analysis/installed-before.json). +- Parser probe: `~/.cargo/bin/uast parse /tmp/codefang-kotlin-repro/Calculator.kt --format json` +- Parser error: `no tree-sitter grammar wired for language kotlin (grammar vendoring pending; see crate docs)`. +- Control: the freshly built checkout analyzes the same repository and reports + 96 functions, cyclomatic total 120 and maximum 5; see + [checkout-result.json](../runs/kotlin-empty-analysis/checkout-result.json). + +## Expected Behavior + +Kotlin source files (`.kt`) and scripts (`.kts`) produce function metrics when +analyzed as files or through a directory walk. The parser supports Kotlin. + +## Actual Behavior + +The installed analyzer returns success with zero functions. Its installed UAST +tool explicitly reports a missing grammar for a nonempty Kotlin class. + +## Root Cause Analysis + +The installed executables differ from the current source. Extension registration +alone does not guarantee grammar availability in an older build. The current +`cf-uast/build.rs` compiles Kotlin's vendored parser and external scanner; +`cf-uast/src/languages.rs` resolves `kotlin` to `tree_sitter_kotlin`. Both are +already present, and the existing Kotlin lowering test passes without changes. +The installed parser rejects the same language, while the new CLI returns real +metrics on the public sample. No parser or analyzer source change is justified +by this reproduction. + +## Fix + +Build and install both binaries from the current checkout. Add a hermetic CLI +regression for a companion-object function, exercising `.kt`, `.kts`, individual +file analysis and a nested directory walk. The test asserts the function name +and counts, so extension-only support or an empty fallback cannot satisfy it. +The fixture follows the structure of the public +[Kotlin Calculator sample](https://github.com/Kotlin/kmp-basic-sample/blob/83e1d17070878ce1ded5936b789bff515aa63e7f/shared/src/commonMain/kotlin/com/jetbrains/basicsample/Calculator.kt). + +Both binaries have been rebuilt and installed. The installed `codefang` now +reports 96 functions on the same public repository +([installed-after.json](../runs/kotlin-empty-analysis/installed-after.json)); +the installed `uast` parses the companion-object fixture and yields one function +([parser-after.json](../runs/kotlin-empty-analysis/parser-after.json)). +Binary hashes and reproduction details are retained in the +[evidence directory](../runs/kotlin-empty-analysis/reproduction.md). + +## Stressors and Validation + +1. Stale executables: compare installed and freshly built results. +2. Extension-only registration: assert nonzero function metrics. +3. Missing parser grammar: probe `uast parse` directly. +4. Missing external scanner: parse the real Kotlin fixture. +5. `.kt` source: analyze it as a file. +6. `.kts` script: analyze it as a file. +7. Nested Gradle source paths: walk `src/main/kotlin`. +8. Companion-object nesting: assert discovery of `sum`. +9. Expression-bodied functions: assert one function for `sum`. +10. Mixed source/script roots: assert two functions in two files. +11. Developer configuration leakage: use the existing isolated CLI test helper. +12. Upstream source changes: retain hermetic test data and pin reproduction links. + +## Traceability + +- Verification: [workspace tests, lint, dead code and installed-tool checks](../runs/kotlin-empty-analysis/verification.md). +- Regression: `bins/codefang/tests/static_walk_e2e.rs:kotlin_sources_and_scripts_produce_function_metrics`. +- Existing grammar wiring: `crates/cf-uast/build.rs`, `crates/cf-uast/src/languages.rs`. +- Operational fix: rebuild and replace `~/.cargo/bin/codefang` and `~/.cargo/bin/uast`. +- User guidance: [Kotlin analysis troubleshooting](../../docs/kotlin-analysis.md). +- Roadmap: [Kotlin analysis repair](ROADMAP.md). diff --git a/specs/bugs/BUG-kotlin-shotness-empty.md b/specs/bugs/BUG-kotlin-shotness-empty.md new file mode 100644 index 00000000..9a21ff9a --- /dev/null +++ b/specs/bugs/BUG-kotlin-shotness-empty.md @@ -0,0 +1,86 @@ +# BUG-kotlin-shotness-empty: Kotlin functions have no Shotness names + +## Summary + +`history/shotness --limit 200 --format text` reports zero nodes for Kotlin +functions even when the Kotlin grammar is available. + +## Reproduction + +- Test: `bins/codefang/tests/shotness_e2e.rs:kotlin_shotness_with_limit_and_text_reports_functions`. +- Command: `cargo test -p codefang --test shotness_e2e kotlin_shotness_with_limit_and_text_reports_functions -- --nocapture`. +- Fixture: a real Git commit containing `fun sum(a: Int, b: Int): Int = a + b`. +- Actual CLI arguments: `run -a history/shotness --limit 200 --format text `. +- Failure: the CLI exits successfully but prints `0 nodes`, `Total Nodes 0` + and `Total Changes 0`; the assertion requiring `1 nodes` fails. +- Captured output: `/tmp/codefang-kotlin-repro/shotness-red.log`. +- Public reproduction: [Spring Petclinic Kotlin](https://github.com/spring-petclinic/spring-petclinic-kotlin/commit/817dba3b02261e4291f138d5d8bb8c85c7d49758), + cloned with real commit history. The exact command also yielded zero nodes. +- [Persistent before/after reports](../runs/kotlin-shotness-empty/reproduction.md). + +## Expected Behavior + +Kotlin functions are named tracked entities, with their enclosing declarations +qualifying their names. Nonempty Kotlin history produces Shotness metrics. + +## Actual Behavior + +Functions are parsed but discarded before Shotness tracking because the default +name query yields no name. + +## Root Cause Analysis + +Shotness uses `.props.name` to identify selected declarations. Kotlin's grammar +does not provide a `name` field for its function declarations. The generic +lowering fills `props.name` only from that field, and Kotlin's mapping does not +capture its direct `simple_identifier` as a name. Static complexity finds the +identifier independently, so the earlier static regression could pass while +Shotness remained empty. Reinstalling the grammar resolves the separate stale +binary problem but does not resolve this defect in the current source. + +## Fix Scope and Validation + +Capture declaration names explicitly in the Kotlin mapping. Keep Shotness's +name-query contract unchanged. Verify through the exact text/limit flags, +machine reports, qualified names, source/script extensions and a public Kotlin +repository with real commit history. + +The function query captures the direct `simple_identifier`; class and object +queries capture their direct `type_identifier`. Named companion objects capture +their identifier before an unconditional fallback supplies Kotlin's implicit +`Companion` name. A condition on the direct `type_identifier` prevents unnamed +companions from adopting a nested named companion's identifier: query matching +searches the subtree, so testing the captured name alone is insufficient. +No Shotness query, +selection flag or reporting behavior is changed. + +The public 200-commit text report changes from zero nodes to 14 nodes, +21 changes and 29 couplings. The test matrix covers an exact text/limit invocation, +`Calculator.sum`, `Service.work`, `Service.Companion.work`, +`Service.Factory.work`, two-commit `.kts` body changes and co-change counts, +plus annotated generic extension functions with unrelated identifiers. +A nested-companion regression asserts the class-name sequence +`Outer`, `Companion`, `Inner`, `Factory`. + +Both corrected executables are installed. The installed text and JSON reports +agree on all three public-repository totals; the installed parser now exposes +`Calculator`, `Companion` and `sum` as declaration names for the earlier +companion-object sample. Captures are stored in +[the evidence directory](../runs/kotlin-shotness-empty/reproduction.md). + +Stressors: missing grammar; missing name properties; unnamed lambdas; annotations +containing identifiers; extension receivers; generic type parameters; nested +classes; companion objects; `.kts` scripts; edited function bodies; custom +Shotness queries; format disagreement. Use direct-child captures to avoid +accidentally naming declarations after annotations, receivers or body references. + +## Traceability + +- Verification: [workspace suite and installed-tool results](../runs/kotlin-shotness-empty/verification.md). +- Regression: `bins/codefang/tests/shotness_e2e.rs`. +- Mapping: `crates/cf-uast-mappings/src/kotlin.rs`. +- Mapping regression: `crates/cf-uast-mappings/tests/shape_contract.rs:kotlin_names_exclude_annotation_receiver_and_body_identifiers`. +- Usage: [Kotlin troubleshooting](../../docs/kotlin-analysis.md) and + [Shotness reference](../../site/analyzers/shotness.md). +- Related static-only investigation: [BUG-kotlin-empty-analysis](BUG-kotlin-empty-analysis.md). +- Roadmap: [Kotlin Shotness repair](ROADMAP.md). diff --git a/specs/bugs/BUG-shotness-language-names.md b/specs/bugs/BUG-shotness-language-names.md new file mode 100644 index 00000000..46d828be --- /dev/null +++ b/specs/bugs/BUG-shotness-language-names.md @@ -0,0 +1,106 @@ +# BUG-shotness-language-names: language mappings drop Shotness entities + +## Summary + +A cross-language audit after the Kotlin fix finds additional named declarations +that parse successfully but produce missing or incorrect Shotness identities, +and declaration headers that exclude body edits from tracked spans. + +## Reproduction + +- Test: `bins/codefang/tests/shotness_languages_e2e.rs`. +- Command: `cargo test -p codefang --test shotness_languages_e2e`. +- Body-edit fixtures create real Git histories with two commits: introduce a + named declaration, then change its body from `111` to `222`. Reference-only + fixtures assert zero entities; the old LaTeX header has an introduction test. +- The real CLI runs `history/shotness --limit 200` in both text and JSON formats. +- Expected: the declared name, one tracked entity per declaration, two touches. +- Initial failures include Clojure, CMake, Common Lisp, Elixir, Elm, Fortran, + GraphQL, Groovy, Make, PowerShell, Protocol Buffers, R, Rego and SQL. + The compiled-registry extension also reproduces missing/wrapped LaTeX names. +- Raw parser audit: `/tmp/codefang-shotness-languages/audit-before.txt`. +- CLI regression capture: `/tmp/codefang-shotness-languages/cli-before-full.log`. + +## Expected Behavior + +The default Shotness query finds named declarations and counts edits throughout +their declaration spans. Anonymous values are excluded. Language keywords, +parameters and identifiers inside a body are not declaration names. + +## Actual Behavior and Root Cause + +Shotness uses the `Function` role and `props.name`. Several mapping tables expose +neither the grammar's actual name location nor the complete declaration span. +Examples include defining forms with positional names (Clojure), names inside +headers (CMake, Common Lisp, Fortran), non-`name` fields (Groovy), and name-wrapper +nodes (GraphQL, PowerShell, LaTeX). Protocol Buffer RPC methods lack the `Function` +role used by the default query. R exposes the keyword `function` as a grammar +field called `name`, which is not the variable receiving a function value. + +Two shared defects also affect name extraction. Query cursors searched nested +expressions before matching the current node: an R right-assignment captured +the literal `111` inside a function instead of its target `probe`. Root-bounded +matching fixes this while retaining descendant captures explicitly present in +the pattern. Separately, grammar-derived `name` fields overwrote explicit mapping +properties, replacing LaTeX's normalized `probe` with `{probe}`. Explicit mapping +names now take precedence over the grammar-field fallback. + +## Fix and Acceptance + +Use explicit language mappings for actual declaration names and full spans. +Preserve the default Shotness DSL and anonymous-value exclusion. Keep headers +structural when their enclosing node represents the function. + +The audit covers representative named declarations in all 42 filename-selected +language mappings that model functions, methods or structural entry points, +plus a CSS reference-only negative case. A registry-derived test requires a +history fixture whenever another function-declaring mapping becomes selectable. +The fixtures cover private definitions, macros, class qualification, guarded +Elixir functions, all five R assignment operators, and anonymous-value exclusion. +R right-assigned function values are parenthesized to preserve their grammar +precedence. Static complexity still measures the named R function body. + +## Audit Boundaries + +- The remaining 24 mappings do not declare functions/methods, including CSS + function references. Plain data files have no default Shotness entities. +- Helm's overlapping `.yaml`/`.yml` extensions select YAML, and `rust_with_rstml` + has no registered filename extension. These two selection limits are asserted + by the registry-coverage test; changing either requires revisiting the matrix. +- The pinned LaTeX grammar models only the header of old-style `\def` commands: + [grammar source](https://github.com/latex-lsp/tree-sitter-latex/blob/f736d24d89acbd90092d92089e5171e6a449db40/grammar.js). + Their names and introduction are tested; body-only edits on later lines + cannot be attributed to that header. New-style commands, + environments, theorems and paired delimiters have body-edit coverage. +- This audit does not assert every dialect, overload or multi-clause form. + It uses registered extensions; conventional `Makefile`/`CMakeLists.txt` names + and the existing Rust `impl` qualification limit are outside this repair. + +## Stressors + +1. A grammar uses an anonymous keyword as its `name` field. +2. A declaration name is inside a header wrapper. +3. A header is counted as a second function. +4. A body edit occurs after the header line. +5. A defining macro uses positional names. +6. A nested declaration has a name before its parent gets one. +7. An anonymous function contains named references. +8. A custom Shotness name/selection query is configured. +9. A method lacks the default query's `Function` role. +10. Two languages share an extension. +11. A fixture uses syntax unsupported by the pinned grammar. +12. The executable on PATH predates the mapping changes. + +## Traceability + +- Evidence: [before/after captures](../runs/shotness-language-names/reproduction.md). +- Validation: [workspace gates and installed-binary verification](../runs/shotness-language-names/verification.md). +- User guidance: [coverage and limits](../../docs/shotness-languages.md). +- Regression fixtures: `bins/codefang/tests/fixtures/shotness_languages/`. +- CLI matrix: `bins/codefang/tests/shotness_languages_e2e.rs`. +- Mapping tables: `crates/cf-uast-mappings/src/`. +- Root matching: `crates/cf-uast-mapping/src/pattern_matcher.rs` and + `crates/cf-uast/tests/lowering_contract.rs`. +- Explicit-name precedence: `crates/cf-uast/src/lowering.rs`. +- Valid syntax and registry coverage: `crates/cf-uast-mappings/tests/shotness_fixtures.rs`. +- Prior issue: [Kotlin Shotness](BUG-kotlin-shotness-empty.md). diff --git a/specs/bugs/ROADMAP.md b/specs/bugs/ROADMAP.md new file mode 100644 index 00000000..fdd4c24e --- /dev/null +++ b/specs/bugs/ROADMAP.md @@ -0,0 +1,43 @@ +# Bug repair roadmap + +## Kotlin analysis repair + +- [x] Reproduce empty results on a public Kotlin project and identify the cause: + [BUG-kotlin-empty-analysis](BUG-kotlin-empty-analysis.md). +- [x] Add CLI regression coverage for Kotlin sources, scripts and nested walks: + [static_walk_e2e.rs](../../bins/codefang/tests/static_walk_e2e.rs). +- [x] Verify workspace tests, lint and dead code checks: + [verification results](../runs/kotlin-empty-analysis/verification.md). +- [x] Replace the installed binaries and verify the public project again: + [before/after evidence](../runs/kotlin-empty-analysis/reproduction.md). +- [x] Document [usage and troubleshooting](../../docs/kotlin-analysis.md). + +## Kotlin Shotness repair + +- [x] Reproduce the screenshot's exact flags on a real Kotlin Git fixture and + public repository: [BUG-kotlin-shotness-empty](BUG-kotlin-shotness-empty.md). +- [x] Capture function and container names in the + [Kotlin mapping](../../crates/cf-uast-mappings/src/kotlin.rs). +- [x] Cover text/limit flags, qualified names and script change/coupling metrics + in [Shotness CLI tests](../../bins/codefang/tests/shotness_e2e.rs). +- [x] Cover annotation, receiver and body identifiers in the + [mapping shape test](../../crates/cf-uast-mappings/tests/shape_contract.rs). +- [x] Verify workspace tests, lint and dead code checks: + [final verification](../runs/kotlin-shotness-empty/verification.md). +- [x] Install corrected binaries and repeat the public repository command: + [installed text report](../runs/kotlin-shotness-empty/public-after.txt) and + [matching JSON totals](../runs/kotlin-shotness-empty/public-after-aggregate.json). +- [x] Correct the earlier static-only conclusion and document Kotlin Shotness. + +## Shotness language-name audit + +- [x] Reproduce naming, selection and body-span gaps across other mappings: + [BUG-shotness-language-names](BUG-shotness-language-names.md). +- [x] Repair the 15 affected language tables and shared capture/name precedence. +- [x] Add a real-history [language matrix](../../bins/codefang/tests/shotness_languages_e2e.rs), + parser-validity checks and a registry-derived coverage gate. +- [x] Document [coverage and parser limits](../../docs/shotness-languages.md). +- [x] Verify workspace tests, lint and dead code checks: + [verification](../runs/shotness-language-names/verification.md). +- [x] Rebuild installed binaries and confirm every mixed-repository name/count: + [installed results](../runs/shotness-language-names/installed-polyglot-summary.json). diff --git a/specs/runs/kotlin-empty-analysis/binary-sha256.txt b/specs/runs/kotlin-empty-analysis/binary-sha256.txt new file mode 100644 index 00000000..f583295b --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/binary-sha256.txt @@ -0,0 +1,4 @@ +de84c4c5ec43bd54bb02ccb23c81023d124020d204281db7fe72a8a5d311fd46 /tmp/codefang-kotlin-repro/previous-bin/codefang +92d5b6f6da10b84b8aebc4c5ee63e45f2f2e0a88290fda9d24eac200d0db6390 /tmp/codefang-kotlin-repro/previous-bin/uast +37a6829eea68172189bba68cd52ca82c0a1b9db73ae01ac0058903583ba2de3a /home/dmitriy/.cargo/bin/codefang +7a1e1e5b46f29dc0b9b04c213d0c888459846d5a23a77d4fb3deb4a43505f4a1 /home/dmitriy/.cargo/bin/uast diff --git a/specs/runs/kotlin-empty-analysis/checkout-result.json b/specs/runs/kotlin-empty-analysis/checkout-result.json new file mode 100644 index 00000000..c2e178af --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/checkout-result.json @@ -0,0 +1,33 @@ +{ + "metrics": [ + { + "label": "Total Functions", + "value": "96" + }, + { + "label": "Avg Complexity", + "value": "1.2" + }, + { + "label": "Max Complexity", + "value": "5" + }, + { + "label": "Total Complexity", + "value": "120" + }, + { + "label": "Cognitive Total", + "value": "30" + }, + { + "label": "Max Nesting", + "value": "3" + }, + { + "label": "Decision Points", + "value": "24" + } + ], + "files_analyzed": 73 +} diff --git a/specs/runs/kotlin-empty-analysis/installed-after.json b/specs/runs/kotlin-empty-analysis/installed-after.json new file mode 100644 index 00000000..c2e178af --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/installed-after.json @@ -0,0 +1,33 @@ +{ + "metrics": [ + { + "label": "Total Functions", + "value": "96" + }, + { + "label": "Avg Complexity", + "value": "1.2" + }, + { + "label": "Max Complexity", + "value": "5" + }, + { + "label": "Total Complexity", + "value": "120" + }, + { + "label": "Cognitive Total", + "value": "30" + }, + { + "label": "Max Nesting", + "value": "3" + }, + { + "label": "Decision Points", + "value": "24" + } + ], + "files_analyzed": 73 +} diff --git a/specs/runs/kotlin-empty-analysis/installed-before.json b/specs/runs/kotlin-empty-analysis/installed-before.json new file mode 100644 index 00000000..dd4ff915 --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/installed-before.json @@ -0,0 +1,30 @@ +{ + "metrics": [ + { + "label": "Total Functions", + "value": "0" + }, + { + "label": "Avg Complexity", + "value": "0.0" + }, + { + "label": "Max Complexity", + "value": "0" + }, + { + "label": "Total Complexity", + "value": "0" + }, + { + "label": "Cognitive Total", + "value": "0" + }, + { + "label": "Decision Points", + "value": "0" + } + ], + "files_analyzed": 20, + "status": "Excellent complexity - functions are simple and maintainable" +} diff --git a/specs/runs/kotlin-empty-analysis/installed-before.stderr b/specs/runs/kotlin-empty-analysis/installed-before.stderr new file mode 100644 index 00000000..4a8db950 --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/installed-before.stderr @@ -0,0 +1,2 @@ +progress: resolved analyzers: static=1 history=0 output_format=json +progress: run finished in 196ms diff --git a/specs/runs/kotlin-empty-analysis/parser-after.json b/specs/runs/kotlin-empty-analysis/parser-after.json new file mode 100644 index 00000000..0cb05752 --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/parser-after.json @@ -0,0 +1,4 @@ +{ + "function_count": 1, + "parse_succeeded": true +} diff --git a/specs/runs/kotlin-empty-analysis/parser-before.stderr b/specs/runs/kotlin-empty-analysis/parser-before.stderr new file mode 100644 index 00000000..52cb3012 --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/parser-before.stderr @@ -0,0 +1 @@ +Error: failed to parse /tmp/codefang-kotlin-repro/Calculator.kt: parse /tmp/codefang-kotlin-repro/Calculator.kt: no tree-sitter grammar wired for language kotlin (grammar vendoring pending; see crate docs) diff --git a/specs/runs/kotlin-empty-analysis/reproduction.md b/specs/runs/kotlin-empty-analysis/reproduction.md new file mode 100644 index 00000000..862159a7 --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/reproduction.md @@ -0,0 +1,25 @@ +# Kotlin reproduction evidence + +The public Spring Petclinic Kotlin archive was downloaded from its `main` +branch. The corresponding repository commit is +`817dba3b02261e4291f138d5d8bb8c85c7d49758`. +The archive's PAX `comment` also confirms that commit. +Archive SHA-256: +`4a7aceec9ff3de3d13eb52ae86eb1f2956b4fc998c74fb265998c7f7c17cfe38`. + +Command used before and after replacing the installed binary: + +```sh +codefang run -a static/complexity --format json --per-file /tmp/codefang-kotlin-repro/spring-petclinic-kotlin-main +``` + +The JSON evidence files retain the metrics and analyzed-file count from the +full reports. Before: zero functions. After: 96 functions, cyclomatic total +120, maximum 5, across 73 analyzed files. + +The original executables are preserved for rollback under +`/tmp/codefang-kotlin-repro/previous-bin/`. Raw reports and build/check logs +are under `/tmp/codefang-kotlin-repro/`. + +The Kotlin companion-object parser probe before replacement failed with the +message retained in `parser-before.stderr`. diff --git a/specs/runs/kotlin-empty-analysis/verification.md b/specs/runs/kotlin-empty-analysis/verification.md new file mode 100644 index 00000000..72bd220e --- /dev/null +++ b/specs/runs/kotlin-empty-analysis/verification.md @@ -0,0 +1,17 @@ +# Verification + +- `make -o submodules test`: exit 0; 3154 tests passed, 1 ignored. +- Includes unit, integration, CLI end-to-end, Markdown examples and Rust doctests. +- `make -o submodules lint`: exit 0; Clippy workspace/all-targets with warnings denied. +- `make -o submodules deadcode`: exit 0; workspace/all-targets check with dead code denied. +- `cargo fmt --all -- --check`: exit 0. +- `make -o submodules build`: exit 0. +- `make -o submodules install`: exit 0; both executables replaced. +- Installed `codefang`: public Kotlin project reports 96 functions (previously 0). +- Installed `uast`: companion-object Kotlin fixture parses and yields one function. + +`-o submodules` uses the existing submodule checkout without invoking Git. +Go-specific vet/race/coverage commands do not apply to this Rust workspace. +No production code changed; the fix replaces installed builds lacking Kotlin support. + +Raw command logs: `/tmp/codefang-kotlin-repro/{test,lint,deadcode,fmt,build,install}.log`. diff --git a/specs/runs/kotlin-shotness-empty/binary-sha256.txt b/specs/runs/kotlin-shotness-empty/binary-sha256.txt new file mode 100644 index 00000000..67447fbf --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/binary-sha256.txt @@ -0,0 +1,2 @@ +9cb4e3833420702bdd633416fbd05d4754c3244f1b7b3cb5583856829a310b96 /home/dmitriy/.cargo/bin/codefang +d8503cb40f9714b1ad26d874d052a1bdd75df83c00ba7f2a37b9f10cb90eaa3a /home/dmitriy/.cargo/bin/uast diff --git a/specs/runs/kotlin-shotness-empty/nested-companion-before.txt b/specs/runs/kotlin-shotness-empty/nested-companion-before.txt new file mode 100644 index 00000000..fd19f745 --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/nested-companion-before.txt @@ -0,0 +1,21 @@ + Compiling cf-uast-mappings v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-uast-mappings) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s + Running tests/shape_contract.rs (target/debug/deps/shape_contract-83039fdd067aa872) + +running 1 test + +thread 'kotlin_implicit_companion_does_not_take_a_nested_companion_name' (3211708) panicked at crates/cf-uast-mappings/tests/shape_contract.rs:57:5: +assertion `left == right` failed + left: ["Outer", "Factory", "Inner", "Factory"] + right: ["Outer", "Companion", "Inner", "Factory"] +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test kotlin_implicit_companion_does_not_take_a_nested_companion_name ... FAILED + +failures: + +failures: + kotlin_implicit_companion_does_not_take_a_nested_companion_name + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 26 filtered out; finished in 3.91s + +error: test failed, to rerun pass `-p cf-uast-mappings --test shape_contract` diff --git a/specs/runs/kotlin-shotness-empty/nested-parser-names.json b/specs/runs/kotlin-shotness-empty/nested-parser-names.json new file mode 100644 index 00000000..4ed9056c --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/nested-parser-names.json @@ -0,0 +1,6 @@ +[ + "Outer", + "Companion", + "Inner", + "Factory" +] diff --git a/specs/runs/kotlin-shotness-empty/parser-names.json b/specs/runs/kotlin-shotness-empty/parser-names.json new file mode 100644 index 00000000..2225c73a --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/parser-names.json @@ -0,0 +1,5 @@ +[ + "Calculator", + "Companion", + "sum" +] diff --git a/specs/runs/kotlin-shotness-empty/public-after-aggregate.json b/specs/runs/kotlin-shotness-empty/public-after-aggregate.json new file mode 100644 index 00000000..5fe84f9e --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/public-after-aggregate.json @@ -0,0 +1,8 @@ +{ + "total_nodes": 14, + "total_changes": 21, + "total_couplings": 29, + "avg_changes_per_node": 1.5, + "avg_coupling_strength": 0.5229885057471265, + "hot_nodes": 0 +} diff --git a/specs/runs/kotlin-shotness-empty/public-after.txt b/specs/runs/kotlin-shotness-empty/public-after.txt new file mode 100644 index 00000000..efb00f69 --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/public-after.txt @@ -0,0 +1,44 @@ +codefang (v2): + version: 0 + hash: +history/shotness: +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Shotness Analysis 14 nodes ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary + ──────────────────────────────────────────────────────────────────────────── + Total Nodes 14 + Total Changes 21 + Avg Changes/Node 1.5 + Total Couplings 29 + Avg Coupling Strength 52% + Hot Nodes 0 + + Hottest Functions + ──────────────────────────────────────────────────────────────────────────── + OwnerControllerTest.s... [████████████████████] 1.0 (3 changes) + OwnerControllerTest.t... [████████████████████] 1.0 (3 changes) + CrashControllerTest.t... [█████████████░░░░░░░] 0.7 (2 changes) + PetTypeFormatterTest.... [█████████████░░░░░░░] 0.7 (2 changes) + VetControllerTest.tes... [█████████████░░░░░░░] 0.7 (2 changes) + MockMvcValidationConf... [██████░░░░░░░░░░░░░░] 0.3 (1 changes) + Owner.getPet (Owner.kt) [██████░░░░░░░░░░░░░░] 0.3 (1 changes) + OwnerController.showO... [██████░░░░░░░░░░░░░░] 0.3 (1 changes) + PetRepositoryTest.sho... [██████░░░░░░░░░░░░░░] 0.3 (1 changes) + PetTypeFormatterTest.... [██████░░░░░░░░░░░░░░] 0.3 (1 changes) + ... and 4 more + + Strongest Couplings + ──────────────────────────────────────────────────────────────────────────── + OwnerCont... ↔ OwnerCont... 67% (2 co-changes) + CrashCont... ↔ MockMvcVa... 50% (1 co-changes) + CrashCont... ↔ PetTypeFo... 50% (1 co-changes) + CrashCont... ↔ VetContro... 50% (1 co-changes) + MockMvcVa... ↔ PetTypeFo... 50% (1 co-changes) + MockMvcVa... ↔ VetContro... 50% (1 co-changes) + Owner.getPet ↔ OwnerCont... 33% (1 co-changes) + Owner.getPet ↔ PetReposi... 100% (1 co-changes) + OwnerCont... ↔ OwnerCont... 33% (1 co-changes) + OwnerCont... ↔ OwnerCont... 33% (1 co-changes) + ... and 19 more diff --git a/specs/runs/kotlin-shotness-empty/public-before.txt b/specs/runs/kotlin-shotness-empty/public-before.txt new file mode 100644 index 00000000..a321e512 --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/public-before.txt @@ -0,0 +1,16 @@ +codefang (v2): + version: 0 + hash: +history/shotness: +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Shotness Analysis 0 nodes ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary + ──────────────────────────────────────────────────────────────────────────── + Total Nodes 0 + Total Changes 0 + Avg Changes/Node 0.0 + Total Couplings 0 + Avg Coupling Strength 0% + Hot Nodes 0 diff --git a/specs/runs/kotlin-shotness-empty/regression-before.txt b/specs/runs/kotlin-shotness-empty/regression-before.txt new file mode 100644 index 00000000..5a6836da --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/regression-before.txt @@ -0,0 +1,36 @@ + Compiling codefang v0.0.0 (/home/dmitriy/sources/codefang/bins/codefang) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.86s + Running tests/shotness_e2e.rs (target/debug/deps/shotness_e2e-4ea08a3c9a3a6b08) + +running 1 test + +thread 'kotlin_shotness_with_limit_and_text_reports_functions' (3099194) panicked at bins/codefang/tests/shotness_e2e.rs:119:5: +codefang (v2): + version: 0 + hash: +history/shotness: +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Shotness Analysis 0 nodes ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary + ──────────────────────────────────────────────────────────────────────────── + Total Nodes 0 + Total Changes 0 + Avg Changes/Node 0.0 + Total Couplings 0 + Avg Coupling Strength 0% + Hot Nodes 0 + + +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test kotlin_shotness_with_limit_and_text_reports_functions ... FAILED + +failures: + +failures: + kotlin_shotness_with_limit_and_text_reports_functions + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 12 filtered out; finished in 3.08s + +error: test failed, to rerun pass `-p codefang --test shotness_e2e` diff --git a/specs/runs/kotlin-shotness-empty/reproduction.md b/specs/runs/kotlin-shotness-empty/reproduction.md new file mode 100644 index 00000000..87f7412f --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/reproduction.md @@ -0,0 +1,27 @@ +# Kotlin Shotness reproduction + +Repository: https://github.com/spring-petclinic/spring-petclinic-kotlin + +HEAD: `817dba3b02261e4291f138d5d8bb8c85c7d49758`. +The repository was cloned through libgit2 with its full commit history. +A libgit2 revision walk confirms 844 commits reachable from HEAD, so the +requested 200-commit bound selects a partial history window. +The private `top-my-credits` repository from the screenshot is not available. + +Exact flags used before and after the source fix: + +```sh +codefang run -a history/shotness --limit 200 --format text /tmp/codefang-kotlin-repro/petclinic-history +``` + +Before: 0 nodes, 0 changes, 0 couplings. +After: 14 nodes, 21 changes, 29 couplings. + +`public-before.txt` and `public-after.txt` retain the text reports. +`regression-before.txt` retains the failing real-binary regression on a +hermetic Kotlin Git fixture. The earlier static-complexity verification did +not exercise Shotness's declaration-name query. + +`nested-companion-before.txt` retains the failing naming-edge regression: +the outer implicit companion must keep its `Companion` name when a nested +class declares a named companion. diff --git a/specs/runs/kotlin-shotness-empty/verification.md b/specs/runs/kotlin-shotness-empty/verification.md new file mode 100644 index 00000000..db7dbf15 --- /dev/null +++ b/specs/runs/kotlin-shotness-empty/verification.md @@ -0,0 +1,20 @@ +# Verification + +- `make -o submodules test`: exit 0; 3162 tests passed, 1 ignored. +- Includes unit, integration, CLI end-to-end, Markdown examples and Rust doctests. +- `make -o submodules lint`: exit 0; warnings denied across all workspace targets. +- `make -o submodules deadcode`: exit 0; dead code denied across all workspace targets. +- `cargo fmt --all -- --check`: exit 0. +- `make -o submodules build`: exit 0. +- `make -o submodules install`: exit 0; both executables replaced. +- Installed `codefang` with the screenshot's exact flags: 14 nodes, 21 changes, 29 couplings. +- Installed JSON and text totals agree. +- Installed `uast`: nested classes have names `Outer`, `Companion`, `Inner`, `Factory`. + +The direct Shotness CLI test suite passes all 18 tests. Eight new regressions +cover the exact flags, class/object/companion qualification, script changes +and couplings, annotated generic extension names, and nested companion names. + +`-o submodules` uses the existing submodule checkout without invoking Git. +Go-specific commands do not apply to this Rust workspace. +Raw logs are under `/tmp/codefang-kotlin-repro/shotness-*.log`. diff --git a/specs/runs/shotness-language-names/binary-sha256.txt b/specs/runs/shotness-language-names/binary-sha256.txt new file mode 100644 index 00000000..70250779 --- /dev/null +++ b/specs/runs/shotness-language-names/binary-sha256.txt @@ -0,0 +1,2 @@ +432f854d036d602c911ac677237dddc8f98d8e217b6ac1f47faacf2ecb5f165f /home/dmitriy/.cargo/bin/codefang +f4727d49da9bd9dd90051ee7faf7b9f020d4154567503cdb7682edd3e4cd44b1 /home/dmitriy/.cargo/bin/uast diff --git a/specs/runs/shotness-language-names/installed-polyglot-summary.json b/specs/runs/shotness-language-names/installed-polyglot-summary.json new file mode 100644 index 00000000..cdd2136d --- /dev/null +++ b/specs/runs/shotness-language-names/installed-polyglot-summary.json @@ -0,0 +1,21 @@ +{ + "files_checked": 63, + "before": { + "total_nodes": 37, + "total_changes": 72, + "total_couplings": 666, + "avg_changes_per_node": 1.945945945945946, + "avg_coupling_strength": 0.9474474474474475, + "hot_nodes": 0 + }, + "after": { + "total_nodes": 71, + "total_changes": 141, + "total_couplings": 2485, + "avg_changes_per_node": 1.9859154929577465, + "avg_coupling_strength": 0.9859154929577465, + "hot_nodes": 0 + }, + "names_and_counts_match_fixtures": true, + "old_latex_header_body_only_change_count": 1 +} diff --git a/specs/runs/shotness-language-names/installed-polyglot.txt b/specs/runs/shotness-language-names/installed-polyglot.txt new file mode 100644 index 00000000..68df2bda --- /dev/null +++ b/specs/runs/shotness-language-names/installed-polyglot.txt @@ -0,0 +1,44 @@ +codefang (v2): + version: 0 + hash: +history/shotness: +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Shotness Analysis 71 nodes ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + Summary + ──────────────────────────────────────────────────────────────────────────── + Total Nodes 71 + Total Changes 141 + Avg Changes/Node 2.0 + Total Couplings 2485 + Avg Coupling Strength 99% + Hot Nodes 0 + + Hottest Functions + ──────────────────────────────────────────────────────────────────────────── + "probe" (gotmpl.gotmpl) [████████████████████] 1.0 (2 changes) + Box.guarded (elixir_g... [████████████████████] 1.0 (2 changes) + Box.macro_guarded (el... [████████████████████] 1.0 (2 changes) + Box.macro_probe (elix... [████████████████████] 1.0 (2 changes) + Box.private_guarded (... [████████████████████] 1.0 (2 changes) + Box.private_macro (el... [████████████████████] 1.0 (2 changes) + Box.private_macro_gua... [████████████████████] 1.0 (2 changes) + Box.private_probe (el... [████████████████████] 1.0 (2 changes) + Box.probe (elixir.ex) [████████████████████] 1.0 (2 changes) + Box.probe (groovy_cla... [████████████████████] 1.0 (2 changes) + ... and 61 more + + Strongest Couplings + ──────────────────────────────────────────────────────────────────────────── + "probe" ↔ Box.guarded 100% (2 co-changes) + "probe" ↔ Box.macro... 100% (2 co-changes) + "probe" ↔ Box.macro... 100% (2 co-changes) + "probe" ↔ Box.priva... 100% (2 co-changes) + "probe" ↔ Box.priva... 100% (2 co-changes) + "probe" ↔ Box.priva... 100% (2 co-changes) + "probe" ↔ Box.priva... 100% (2 co-changes) + "probe" ↔ Box.probe 100% (2 co-changes) + "probe" ↔ Box.probe 100% (2 co-changes) + "probe" ↔ Box.probe 100% (2 co-changes) + ... and 2475 more diff --git a/specs/runs/shotness-language-names/latex-name-before.txt b/specs/runs/shotness-language-names/latex-name-before.txt new file mode 100644 index 00000000..4730ae7c --- /dev/null +++ b/specs/runs/shotness-language-names/latex-name-before.txt @@ -0,0 +1,24 @@ + Compiling cf-uast-mappings v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-uast-mappings) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.54s + Running tests/shotness_fixtures.rs (target/debug/deps/shotness_fixtures-9c99731ba5a4c900) + +running 1 test +test latex_old_definition_exposes_its_declared_name ... FAILED + +failures: + +---- latex_old_definition_exposes_its_declared_name stdout ---- + +thread 'latex_old_definition_exposes_its_declared_name' (967189) panicked at crates/cf-uast-mappings/tests/shotness_fixtures.rs:33:5: +assertion `left == right` failed + left: None + right: Some("\\probe") +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + latex_old_definition_exposes_its_declared_name + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1 filtered out; finished in 2.49s + +error: test failed, to rerun pass `-p cf-uast-mappings --test shotness_fixtures` diff --git a/specs/runs/shotness-language-names/matrix-after.txt b/specs/runs/shotness-language-names/matrix-after.txt new file mode 100644 index 00000000..6b84331c --- /dev/null +++ b/specs/runs/shotness-language-names/matrix-after.txt @@ -0,0 +1,71 @@ + Compiling codefang v0.0.0 (/home/dmitriy/sources/codefang/bins/codefang) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.35s + Running tests/shotness_languages_e2e.rs (target/debug/deps/shotness_languages_e2e-f8d9b0204da043f4) + +running 64 tests +test gotmpl ... ok +test graphql ... ok +test css ... ok +test fish ... ok +test graphql_anonymous ... ok +test clojure ... ok +test clojure_forms ... ok +test cmake_macro ... ok +test cmake ... ok +test graphql_fragment ... ok +test elm ... ok +test commonlisp ... ok +test go ... ok +test groovy_class ... ok +test commonlisp_forms ... ok +test lua ... ok +test groovy ... ok +test javascript ... ok +test make ... ok +test bash ... ok +test c ... ok +test proto ... ok +test java ... ok +test prql ... ok +test elixir_guarded ... ok +test powershell_class ... ok +test powershell ... ok +test r ... ok +test elixir_forms ... ok +test r_anonymous ... ok +test r_right ... ok +test r_super_left ... ok +test r_equal ... ok +test elixir ... ok +test rego ... ok +test r_super_right ... ok +test r_named_function_body_is_measured_by_static_complexity ... ok +test tcl ... ok +test perl ... ok +test latex_old_definition_is_named_when_introduced ... ok +test python ... ok +test zig ... ok +test php ... ok +test haskell ... ok +test fortran ... ok +test fortran_program ... ok +test dart ... ok +test latex ... ok +test rust ... ok +test latex_environment ... ok +test latex_theorem ... ok +test fortran_subroutine ... ok +test scala ... ok +test latex_delimiter ... ok +test sql ... ok +test typescript ... ok +test tsx ... ok +test ruby ... ok +test kotlin ... ok +test cpp ... ok +test nim ... ok +test c_sharp ... ok +test swift ... ok +test crystal ... ok + +test result: ok. 64 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 20.64s diff --git a/specs/runs/shotness-language-names/matrix-before.txt b/specs/runs/shotness-language-names/matrix-before.txt new file mode 100644 index 00000000..4ce31fef --- /dev/null +++ b/specs/runs/shotness-language-names/matrix-before.txt @@ -0,0 +1,167 @@ + Compiling codefang v0.0.0 (/home/dmitriy/sources/codefang/bins/codefang) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.25s + Running tests/shotness_languages_e2e.rs (target/debug/deps/shotness_languages_e2e-f8d9b0204da043f4) + +running 40 tests +test clojure ... FAILED +test elm ... FAILED +test fish ... ok +test gotmpl ... ok +test graphql ... FAILED +test lua ... ok +test cmake ... FAILED +test make ... FAILED +test groovy ... FAILED +test go ... ok +test commonlisp ... FAILED +test javascript ... ok +test prql ... ok +test rego ... FAILED +test proto ... FAILED +test powershell ... FAILED +test tcl ... ok +test r ... FAILED +test bash ... ok +test zig ... ok +test elixir ... FAILED +test java ... ok +test perl ... ok +test c ... ok +test fortran ... FAILED +test dart ... ok +test php ... ok +test rust ... ok +test tsx ... ok +test haskell ... ok +test scala ... ok +test typescript ... ok +test ruby ... ok +test sql ... FAILED +test swift ... ok +test kotlin ... ok +test cpp ... ok +test nim ... ok +test c_sharp ... ok +test crystal ... ok + +failures: + +---- clojure stdout ---- + +thread 'clojure' (815306) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: clojure.clj: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +---- elm stdout ---- + +thread 'elm' (815313) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: elm.elm: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- graphql stdout ---- + +thread 'graphql' (815318) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: graphql.graphql: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- cmake stdout ---- + +thread 'cmake' (815307) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: cmake.cmake: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- make stdout ---- + +thread 'make' (815325) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: make.makefile: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- groovy stdout ---- + +thread 'groovy' (815319) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: groovy.groovy: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- commonlisp stdout ---- + +thread 'commonlisp' (815308) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: commonlisp.lisp: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- rego stdout ---- + +thread 'rego' (815477) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: rego.rego: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- proto stdout ---- + +thread 'proto' (815435) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: proto.proto: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- powershell stdout ---- + +thread 'powershell' (815415) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: powershell.ps1: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- r stdout ---- + +thread 'r' (815474) panicked at bins/codefang/tests/shotness_languages_e2e.rs:42:5: +assertion `left == right` failed: r.r: {"node_hotness":[{"name":"function","type":"Function","file":"r.r","change_count":2,"coupled_nodes":0,"hotness_score":1}],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":1,"total_changes":2,"total_couplings":0,"avg_changes_per_node":2,"avg_coupling_strength":0,"hot_nodes":0}} + left: String("function") + right: "probe" + +---- elixir stdout ---- + +thread 'elixir' (815312) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: elixir.ex: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + +---- fortran stdout ---- + +thread 'fortran' (815315) panicked at bins/codefang/tests/shotness_languages_e2e.rs:43:5: +assertion `left == right` failed: body edit missing in fortran.f90: {"node_hotness":[{"name":"probe","type":"Function","file":"fortran.f90","change_count":1,"coupled_nodes":0,"hotness_score":1}],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":1,"total_changes":1,"total_couplings":0,"avg_changes_per_node":1,"avg_coupling_strength":0,"hot_nodes":0}} + left: Number(1) + right: 2 + +---- sql stdout ---- + +thread 'sql' (815510) panicked at bins/codefang/tests/shotness_languages_e2e.rs:41:5: +assertion `left == right` failed: sql.sql: {"node_hotness":[],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":0,"total_changes":0,"total_couplings":0,"avg_changes_per_node":0,"avg_coupling_strength":0,"hot_nodes":0}} + left: 0 + right: 1 + + +failures: + clojure + cmake + commonlisp + elixir + elm + fortran + graphql + groovy + make + powershell + proto + r + rego + sql + +test result: FAILED. 26 passed; 14 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.61s + +error: test failed, to rerun pass `-p codefang --test shotness_languages_e2e` diff --git a/specs/runs/shotness-language-names/name-precedence-before.txt b/specs/runs/shotness-language-names/name-precedence-before.txt new file mode 100644 index 00000000..795ac0ee --- /dev/null +++ b/specs/runs/shotness-language-names/name-precedence-before.txt @@ -0,0 +1,27 @@ + Compiling cf-uast-mappings v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-uast-mappings) + Compiling cf-uast v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-uast) + Compiling cf-commands v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-commands) + Compiling codefang v0.0.0 (/home/dmitriy/sources/codefang/bins/codefang) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.68s + Running tests/shotness_languages_e2e.rs (target/debug/deps/shotness_languages_e2e-f8d9b0204da043f4) + +running 1 test +test latex_environment ... FAILED + +failures: + +---- latex_environment stdout ---- + +thread 'latex_environment' (972254) panicked at bins/codefang/tests/shotness_languages_e2e.rs:48:5: +assertion `left == right` failed: latex_environment.tex: {"node_hotness":[{"name":"{probe}","type":"Function","file":"latex_environment.tex","change_count":2,"coupled_nodes":0,"hotness_score":1}],"node_coupling":null,"hotspot_nodes":null,"aggregate":{"total_nodes":1,"total_changes":2,"total_couplings":0,"avg_changes_per_node":2,"avg_coupling_strength":0,"hot_nodes":0}} + left: ["{probe}"] + right: ["probe"] +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + latex_environment + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 61 filtered out; finished in 2.09s + +error: test failed, to rerun pass `-p codefang --test shotness_languages_e2e` diff --git a/specs/runs/shotness-language-names/registry.tsv b/specs/runs/shotness-language-names/registry.tsv new file mode 100644 index 00000000..fbfaac79 --- /dev/null +++ b/specs/runs/shotness-language-names/registry.tsv @@ -0,0 +1,46 @@ +TOTAL 68 +bash [".sh", ".bash", ".bashrc", ".bash_profile", ".zshrc", ".zsh"] ["function_definition"] +c [".c", ".h"] ["function_definition", "preproc_function_def"] +c_sharp [".cs"] ["method_declaration", "constructor_declaration", "destructor_declaration", "local_function_statement", "operator_declaration", "conversion_operator_declaration"] +clojure [".clj", ".cljs", ".cljc", ".edn"] ["list_defn", "list_defn_private", "list_defmacro", "list_deftest", "list_defmethod", "anon_fn_lit"] +cmake [".cmake"] ["function_def", "macro_def"] +commonlisp [".lisp", ".lsp"] ["defun"] +cpp [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h"] ["destructor_name", "function_definition", "preproc_function_def", "template_function", "generic_expression", "template_method", "default_method_clause", "delete_method_clause"] +crystal [".cr"] ["method_def", "abstract_method_def", "macro_def", "proc", "method_proc", "fun_def"] +css [".css"] ["function_name"] +dart [".dart"] ["function_signature", "function_expression", "lambda_expression", "method_signature", "getter_signature", "setter_signature", "constructor_signature", "factory_constructor_signature", "local_function_declaration", "operator_signature", "constant_constructor_signature", "redirecting_factory_constructor_signature", "Function"] +elixir [".ex", ".exs"] ["call_def", "call_defp", "call_defmacro", "call_defmacrop", "anonymous_function"] +elm [".elm"] ["named_function", "port_annotation"] +fish [".fish"] ["function_definition"] +fortran [".f", ".f90", ".f95"] ["program", "function", "subroutine", "preproc_function_def"] +go [".go"] ["function_declaration", "method_declaration", "method_elem"] +gotmpl [".gotmpl", ".go.tmpl"] ["define_action"] +graphql [".graphql", ".gql"] ["operation_definition", "fragment_definition"] +groovy [".groovy", ".gradle"] ["function_definition", "closure"] +haskell [".hs", ".lhs"] ["function", "bind_infix_do", "bind_infix_lambda", "bind_do", "bind_lambda", "lambda", "right_section", "left_section"] +helm [".yaml", ".yml"] ["function_call", "method_call"] +java [".java"] ["compact_constructor_declaration", "constructor_declaration", "method_declaration"] +javascript [".js", ".jsx", ".mjs"] ["field_definition_arrow", "field_definition_funcexpr", "function_declaration", "function_expression", "generator_function", "generator_function_declaration", "method_definition", "variable_declarator_arrow", "variable_declarator_funcexpr"] +kotlin [".kt", ".kts"] ["function_declaration", "anonymous_function", "primary_constructor", "secondary_constructor"] +latex [".tex", ".ltx"] ["new_command_definition", "environment_definition", "old_command_definition", "theorem_definition", "paired_delimiter_definition"] +lua [".lua"] ["function_declaration", "assignment_function", "field_function"] +make [".makefile"] ["rule"] +nim [".nim"] ["proc_declaration", "func_declaration", "method_declaration", "iterator_declaration", "converter_declaration", "macro_declaration", "template_declaration"] +perl [".pl", ".pm", ".t"] ["subroutine_declaration_statement"] +php [".php", ".phtml"] ["function_definition", "method_declaration", "property_hook"] +powershell [".ps1", ".psm1", ".psd1"] ["function_statement", "class_method_definition"] +proto [".proto"] ["rpc"] +prql [".prql"] ["function_definition"] +python [".py", ".pyw", ".pyi"] ["function_definition"] +r [".r", ".R", ".rmd", ".Rmd"] ["assigned_function", "assigned_equal", "assigned_super_left", "assigned_right", "assigned_super_right"] +rego [".rego"] ["rule"] +ruby [".rb", ".rbw"] ["method", "singleton_method"] +rust [".rs"] ["function_item", "function_signature_item"] +rust_with_rstml [] ["function_item", "function_modifiers", "function_signature_item", "function_type", "generic_function"] +scala [".scala", ".sc"] ["function_definition", "function_declaration", "lambda_expression", "extension_definition"] +sql [".sql"] ["create_function"] +swift [".swift"] ["function_declaration", "protocol_function_declaration", "init_declaration", "deinit_declaration"] +tcl [".tcl"] ["procedure"] +tsx [".tsx"] ["abstract_method_signature", "function_declaration", "function_expression", "function_signature", "function_type", "generator_function", "generator_function_declaration", "method_definition", "method_signature", "public_field_definition_arrow", "public_field_definition_funcexpr", "variable_declarator_arrow", "variable_declarator_funcexpr"] +typescript [".ts"] ["abstract_method_signature", "function_declaration", "function_expression", "function_signature", "function_type", "generator_function", "generator_function_declaration", "method_definition", "method_signature", "public_field_definition_arrow", "public_field_definition_funcexpr", "variable_declarator_arrow", "variable_declarator_funcexpr"] +zig [".zig"] ["function_declaration", "test_declaration"] diff --git a/specs/runs/shotness-language-names/reproduction.md b/specs/runs/shotness-language-names/reproduction.md new file mode 100644 index 00000000..a10c0d75 --- /dev/null +++ b/specs/runs/shotness-language-names/reproduction.md @@ -0,0 +1,37 @@ +# Shotness language audit evidence + +The initial real-CLI matrix had 26 passing and 14 failing language paths. +`matrix-before.txt` retains those failures. Further compiled-registry inspection +added LaTeX missing/wrapped names; `latex-name-before.txt` and +`name-precedence-before.txt` retain its name-extraction failures. +`root-capture-before.txt` records a matcher returning `111` instead of `probe`. + +The final matrix has 64 tests covering representative declarations in all 42 +filename-selected function-declaring mappings, with CSS reference-only controls, +related declaration forms and a static-complexity cross-check. The fixtures have +no parser errors. A registry-derived gate prevents silently omitting another +selectable function-declaring mapping. + +Run the versioned regression matrix: + +```sh +cargo test -p codefang --test shotness_languages_e2e +cargo test -p cf-uast-mappings --test shotness_fixtures +``` + +Installed-binary verification also used one real repository containing all 63 +fixture files, with introduction and body-edit commits: + +```sh +codefang run -a history/shotness --limit 200 --format text /tmp/cf-gitlib-test-1117334-1794154026237746179-0 +codefang run -a history/shotness --limit 200 --format json /tmp/cf-gitlib-test-1117334-1794154026237746179-0 +``` + +Every reported name and touch count was compared with the versioned fixtures. +The old-style LaTeX definition has a later-line body edit outside the grammar's +header node and retains one introduction touch. The other named entities have +two touches. See `installed-polyglot-summary.json` and `installed-polyglot.txt`. +The previous binary reported 37 entities; the corrected binary reports 71. + +Registry routing and grammar limits are documented in +[the coverage guide](../../../docs/shotness-languages.md). diff --git a/specs/runs/shotness-language-names/root-capture-before.txt b/specs/runs/shotness-language-names/root-capture-before.txt new file mode 100644 index 00000000..35ca41ff --- /dev/null +++ b/specs/runs/shotness-language-names/root-capture-before.txt @@ -0,0 +1,21 @@ + Compiling cf-uast v0.0.0 (/home/dmitriy/sources/codefang/crates/cf-uast) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s + Running tests/lowering_contract.rs (target/debug/deps/lowering_contract-71240999b4bed43d) + +running 1 test + +thread 'mapping_capture_matches_the_current_node_before_nested_expressions' (917359) panicked at crates/cf-uast/tests/lowering_contract.rs:191:5: +assertion `left == right` failed + left: "111" + right: "probe" +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test mapping_capture_matches_the_current_node_before_nested_expressions ... FAILED + +failures: + +failures: + mapping_capture_matches_the_current_node_before_nested_expressions + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.01s + +error: test failed, to rerun pass `-p cf-uast --test lowering_contract` diff --git a/specs/runs/shotness-language-names/verification.md b/specs/runs/shotness-language-names/verification.md new file mode 100644 index 00000000..69442ffc --- /dev/null +++ b/specs/runs/shotness-language-names/verification.md @@ -0,0 +1,22 @@ +# Verification + +- Workspace suite: 3230 tests passed, 1 ignored, zero failures. +- Command: `TMPDIR="$PWD/.tmp/shotness-doc-tests" make -o submodules test`. +- Includes unit, integration, 64 language-matrix tests, Markdown examples and doctests. +- `make -o submodules lint`: exit 0; Clippy across workspace/all-targets with warnings denied. +- `make -o submodules deadcode`: exit 0; dead code denied across workspace/all-targets. +- `cargo fmt --all -- --check`: exit 0. +- `make -o submodules build`: exit 0. +- `make -o submodules install`: exit 0; both installed binaries replaced. +- Installed mixed-repository JSON/text reports agree: 71 entities and 141 touches. +- Every name and per-entity count matches the 63 fixture-file expectations. +- Previous binary: 37 entities and 72 touches on the same repository. + +The first workspace run passed runtime tests but six doctest links crashed with +SIGBUS under `/tmp`. Those six doctests passed with disk-backed temporary files; +the entire workspace rerun with the same temporary-file setting also passed. +No checks were disabled. The root cause of the linker crash is not established. + +`-o submodules` uses the existing submodule checkout without invoking Git. +Raw logs: `/tmp/codefang-shotness-languages/`. +Rollback binaries: `/tmp/codefang-shotness-languages/before-bin/`.