├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── cmd └── lcom4 │ └── main.go ├── go.mod ├── go.sum ├── lcom4.go ├── lcom4_test.go └── testdata └── src └── a └── a.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Fetch all tags 22 | run: git fetch --force --tags 23 | - 24 | name: Set up Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version: 1.18 28 | - 29 | name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | dist/ 3 | cover.* 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | main: ./cmd/lcom4 17 | archives: 18 | - replacements: 19 | darwin: Darwin 20 | linux: Linux 21 | windows: Windows 22 | 386: i386 23 | amd64: x86_64 24 | checksum: 25 | name_template: 'checksums.txt' 26 | snapshot: 27 | name_template: "{{ incpatch .Version }}-next" 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | © LY Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LCOM4go 2 | > LCOM4go is a tool to compute LCOM4, Lack of Cohesion of Methods metrics ver.4, for golang projects. 3 | 4 | **⚠️ This project is archived and no longer maintained.** 5 | 6 | --- 7 | 8 | ## 📌 Notice 9 | 10 | This project has been **archived** as of August 2025. 11 | We no longer maintain it. 12 | 13 | ### Why? 14 | 15 | LCOM4go was created to measure code readability using the LCOM4 (Lack of Cohesion of Methods) metric. 16 | However, as **AI-based code generation becomes the standard**, traditional readability metrics have become less meaningful. 17 | 18 | Instead, we are shifting our focus toward: 19 | 20 | - **AI-friendliness** – Is the code easy for LLMs to understand and modify? 21 | - **Prompt-based regeneration** – Can the code be reliably edited via prompts? 22 | - **Next-gen maintainability** – Beyond cohesion, toward metrics like “promptability” or “regenerability” 23 | 24 | In a world where code is generated, not written by hand, 25 | **maintainability must also evolve.** 26 | 27 | --- 28 | 29 | # Install 30 | ``` 31 | $ go install --ldflags "-s -w" --trimpath github.com/yahoojapan/lcom4go/cmd/lcom4@latest 32 | ``` 33 | 34 | # Usage 35 | ### Directory use installed binary (recommended) 36 | 37 | ``` 38 | $ $(go env GOPATH)/bin/lcom4 ./... 39 | ... 40 | 41 | $ $(go env GOPATH)/bin/lcom4 net/http 42 | ... 43 | ``` 44 | 45 | ### Through the go vet 46 | 47 | ``` 48 | $ go vet -vettool=$(go env GOPATH)/bin/lcom4 ./... 49 | ... 50 | 51 | $ go vet -vettool=$(go env GOPATH)/bin/lcom4 net/http 52 | ... 53 | ``` 54 | 55 | # LCOM4 definition 56 | https://objectscriptquality.com/docs/metrics/lack-cohesion-methods-lcom4 57 | 58 | 59 | # Examples 60 | 61 | The lcom4 of `s0` is 1 because both `method1` and `method2` use `s0.m`. 62 | ``` 63 | type s0 struct { 64 | m int 65 | } 66 | 67 | func (a s0) method1() int { 68 | return a.m 69 | } 70 | func (a s0) method2() int { 71 | return -a.m 72 | } 73 | ``` 74 | 75 | 76 | The lcom4 of `s1` is 2 because `method3` uses `a.n` which is not used by `method1` and `method2`. 77 | ``` 78 | type s1 struct { 79 | m int 80 | n int 81 | } 82 | 83 | func (a s1) method1() int { 84 | return a.m 85 | } 86 | func (a s1) method2() int { 87 | return -a.m 88 | } 89 | func (a s1) method3() int { 90 | return -a.n 91 | } 92 | ``` 93 | 94 | 95 | # Running the tests 96 | ``` 97 | go test ./... 98 | ``` 99 | 100 | 101 | # License 102 | 103 | This software is released under the MIT License, see the license file. 104 | # References 105 | * https://www.aivosto.com/project/help/pm-oo-cohesion.html#LCOM4 106 | * https://kenchon.github.io/cohesive-code 107 | * https://objectscriptquality.com/docs/metrics/lack-cohesion-methods-lcom4 108 | * https://github.com/FujiHaruka/eslint-plugin-lcom 109 | * https://metacpan.org/release/JOENIO/Analizo-1.20.3/source/lib/Analizo/Metric/LackOfCohesionOfMethods.pm 110 | * https://github.com/potfur/lcom 111 | * http://www.isys.uni-klu.ac.at/PDF/1995-0043-MHBM.pdf 112 | * https://github.com/cleuton/jqana 113 | -------------------------------------------------------------------------------- /cmd/lcom4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | lcom4 "github.com/yahoojapan/lcom4go" 5 | "golang.org/x/tools/go/analysis/singlechecker" 6 | ) 7 | 8 | func main() { singlechecker.Main(lcom4.Analyzer) } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yahoojapan/lcom4go 2 | 3 | go 1.18 4 | 5 | require golang.org/x/tools v0.1.10 6 | 7 | require ( 8 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 9 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect 10 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 2 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 3 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 4 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 6 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 7 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 8 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /lcom4.go: -------------------------------------------------------------------------------- 1 | package lcom4 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | "go/types" 8 | "strings" 9 | 10 | "golang.org/x/tools/go/analysis" 11 | ) 12 | 13 | const ( 14 | reportmsg = "'%s' has low cohesion, LCOM4 is %d, pairs of methods: %v" 15 | ) 16 | 17 | const doc = "lcom4go caluculates cohesion metrics value" 18 | 19 | // Analyzer is the lcom4 analyzer. 20 | var Analyzer = &analysis.Analyzer{ 21 | Name: "lcom4", 22 | Doc: doc, 23 | Run: run, 24 | Requires: []*analysis.Analyzer{}, 25 | } 26 | 27 | const ( 28 | field = iota 29 | method 30 | ) 31 | 32 | type graphNode interface { 33 | typ() int 34 | name() string 35 | } 36 | 37 | type fieldNode string 38 | 39 | func (f fieldNode) typ() int { return field } 40 | func (f fieldNode) name() string { return string(f) } 41 | func (f fieldNode) String() string { return fmt.Sprintf(".%s", string(f)) } 42 | 43 | type methodNode string 44 | 45 | func (m methodNode) typ() int { return method } 46 | func (m methodNode) name() string { return string(m) } 47 | func (f methodNode) String() string { return fmt.Sprintf("%s()", string(f)) } 48 | 49 | type graph struct { 50 | nodes []graphNode 51 | neighbor map[graphNode][]graphNode 52 | } 53 | 54 | type graphs map[types.Object]graph 55 | 56 | func initGraph(pkg *types.Package) graphs { 57 | graphs := map[types.Object]graph{} 58 | scope := pkg.Scope() 59 | for _, name := range scope.Names() { 60 | o := scope.Lookup(name) 61 | if _, ok := o.(*types.TypeName); !ok { 62 | continue 63 | } 64 | if _, ok := o.Type().(*types.Named); !ok { 65 | continue 66 | } 67 | // skip 'type xxx interface {...}' 68 | if _, ok := o.Type().Underlying().(*types.Interface); ok { 69 | continue 70 | } 71 | g := graph{nil, map[graphNode][]graphNode{}} 72 | ms := collectMethods(o) 73 | g.nodes = append(g.nodes, ms...) 74 | graphs[o] = g 75 | } 76 | return graphs 77 | } 78 | 79 | func collectMethods(o types.Object) []graphNode { 80 | var nodes []graphNode 81 | 82 | named, ok := o.Type().(*types.Named) 83 | if !ok { 84 | return nil 85 | } 86 | for i := 0; i < named.NumMethods(); i++ { 87 | m := named.Method(i) 88 | nodes = append(nodes, methodNode(m.Name())) 89 | } 90 | return nodes 91 | } 92 | 93 | func collectComments(pass *analysis.Pass) []ast.CommentMap { 94 | var ret []ast.CommentMap 95 | for _, f := range pass.Files { 96 | m := ast.NewCommentMap(pass.Fset, f, f.Comments) 97 | ret = append(ret, m) 98 | } 99 | return ret 100 | } 101 | 102 | func fillNeighbor(graphs map[types.Object]graph, pass *analysis.Pass) { 103 | for _, f := range pass.Files { 104 | ast.Inspect(f, func(node ast.Node) bool { 105 | switch fdecl := node.(type) { 106 | case *ast.FuncDecl: 107 | if fdecl.Recv == nil { 108 | break 109 | } 110 | if len(fdecl.Recv.List[0].Names) == 0 { 111 | break 112 | } 113 | recvType := pass.TypesInfo.TypeOf(fdecl.Recv.List[0].Type) 114 | if p, ok := recvType.(*types.Pointer); ok { 115 | recvType = p.Elem() 116 | } 117 | nd, ok := recvType.(*types.Named) 118 | if !ok { 119 | break 120 | } 121 | graph := graphs[nd.Obj()] 122 | 123 | recvObj := pass.TypesInfo.ObjectOf(fdecl.Recv.List[0].Names[0]) 124 | 125 | ast.Inspect(fdecl.Body, func(node ast.Node) bool { 126 | switch nd := node.(type) { 127 | case *ast.SelectorExpr: 128 | xx, ok := nd.X.(*ast.Ident) 129 | if !ok { 130 | break 131 | } 132 | o := pass.TypesInfo.ObjectOf(xx) 133 | if recvObj != o { 134 | break 135 | } 136 | o2 := pass.TypesInfo.ObjectOf(nd.Sel) 137 | src := methodNode(fdecl.Name.Name) 138 | var dst graphNode 139 | if _, ok := o2.(*types.Var); ok { 140 | dst = fieldNode(nd.Sel.Name) 141 | } else if _, ok := o2.(*types.Func); ok { 142 | dst = methodNode(nd.Sel.Name) 143 | } 144 | graph.neighbor[src] = append(graph.neighbor[src], dst) 145 | graph.neighbor[dst] = append(graph.neighbor[dst], src) 146 | return false 147 | case *ast.Ident: 148 | o := pass.TypesInfo.ObjectOf(nd) 149 | if recvObj == o { 150 | src := methodNode(fdecl.Name.Name) 151 | dst := fieldNode("__receiver__") 152 | graph.neighbor[src] = append(graph.neighbor[src], dst) 153 | graph.neighbor[dst] = append(graph.neighbor[dst], src) 154 | } 155 | } 156 | return true 157 | }) 158 | return false 159 | } 160 | return true 161 | }) 162 | } 163 | 164 | } 165 | 166 | func computeConnectedComponents(g graph) [][]graphNode { 167 | components := [][]graphNode{} 168 | 169 | visited := make(map[graphNode]bool) 170 | for _, n := range g.nodes { 171 | if visited[n] { 172 | continue 173 | } 174 | 175 | compo := collectConnectedNodes(g, n) 176 | for _, m := range compo { 177 | visited[m] = true 178 | } 179 | components = append(components, compo) 180 | } 181 | return components 182 | } 183 | 184 | func collectConnectedNodes(g graph, n graphNode) []graphNode { 185 | var nodes []graphNode 186 | visited := make(map[graphNode]bool) 187 | q := []graphNode{n} 188 | for len(q) > 0 { 189 | head := q[0] 190 | q = q[1:] 191 | if visited[head] { 192 | continue 193 | } 194 | nodes = append(nodes, head) 195 | visited[head] = true 196 | q = append(q, g.neighbor[head]...) 197 | } 198 | return nodes 199 | } 200 | 201 | // ignore comment is: '/lint:ignore lcom4[,...,...] reason' 202 | func hasIgnoreComment(obj types.Object, fset *token.FileSet, cmaps []ast.CommentMap) bool { 203 | for _, cmap := range cmaps { 204 | for node, cgs := range cmap { 205 | cline := fset.File(node.Pos()).Line(node.Pos()) 206 | oline := fset.File(obj.Pos()).Line(obj.Pos()) 207 | if cline != oline { 208 | continue 209 | } 210 | for _, cg := range cgs { 211 | for _, cmt := range cg.List { 212 | if !strings.HasPrefix(cmt.Text, "//") { 213 | continue 214 | } 215 | spl := strings.Split(cmt.Text[2:], " ") 216 | if len(spl) < 3 { 217 | continue 218 | } 219 | if spl[0] != "lint:ignore" { 220 | continue 221 | } 222 | for _, checkee := range strings.Split(spl[1], ",") { 223 | if checkee == "lcom4" { 224 | return true 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } 231 | return false 232 | } 233 | 234 | func run(pass *analysis.Pass) (interface{}, error) { 235 | graphs := initGraph(pass.Pkg) 236 | fillNeighbor(graphs, pass) 237 | cmaps := collectComments(pass) 238 | 239 | for obj, g := range graphs { 240 | components := computeConnectedComponents(g) 241 | if len(components) > 1 && !hasIgnoreComment(obj, pass.Fset, cmaps) { 242 | pass.Reportf(obj.Pos(), fmt.Sprintf(reportmsg, obj.Id(), len(components), components)) 243 | } 244 | } 245 | 246 | return nil, nil 247 | } 248 | -------------------------------------------------------------------------------- /lcom4_test.go: -------------------------------------------------------------------------------- 1 | package lcom4_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "golang.org/x/tools/go/analysis/analysistest" 8 | 9 | lcom4 "github.com/yahoojapan/lcom4go" 10 | ) 11 | 12 | func TestAnalyzer(t *testing.T) { 13 | testdata, _ := filepath.Abs("testdata") 14 | analysistest.Run(t, testdata, lcom4.Analyzer, "a") 15 | } 16 | -------------------------------------------------------------------------------- /testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type s0 struct { // want ".low cohesion, lcom is 2.*" 4 | } 5 | 6 | func (s s0) method1() {} 7 | func (s s0) method2() {} 8 | 9 | // receiver as a field 10 | type s1 string 11 | 12 | func (s s1) method1() string { 13 | return string(s) 14 | } 15 | func (s s1) method2() string { 16 | return string(s) 17 | } 18 | 19 | // embed 20 | type embeddee struct { 21 | a int 22 | } 23 | 24 | func (e embeddee) method1() {} 25 | 26 | type embedder struct { 27 | embeddee 28 | } 29 | 30 | func (e embedder) method2() {} 31 | 32 | // pointer and value receiver 33 | type s2 struct { 34 | m int 35 | } 36 | 37 | func (a *s2) method1() int { 38 | return a.m 39 | } 40 | func (a s2) method2() int { 41 | return a.m 42 | } 43 | 44 | type s3 struct { 45 | m int 46 | } 47 | 48 | func (a s3) method1() int { 49 | return a.m 50 | } 51 | func (a s3) method2() int { 52 | return a.m 53 | } 54 | --------------------------------------------------------------------------------