github.com/dancsecs/gotomd@v0.0.0-20240310162206-65c4805cf510/go_package.go (about) 1 /* 2 Golang To Github Markdown Utility: gotomd 3 Copyright (C) 2023, 2024 Leslie Dancsecs 4 5 This program is free software: you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published by 7 the Free Software Foundation, either version 3 of the License, or 8 (at your option) any later version. 9 10 This program is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 package main 20 21 import ( 22 "fmt" 23 "go/doc" 24 "go/parser" 25 "go/token" 26 "log" 27 "os" 28 "path/filepath" 29 "strings" 30 ) 31 32 const pkgLabel = "package" 33 34 type packageInfo struct { 35 fSet *token.FileSet 36 docPkg *doc.Package 37 functions map[string]*doc.Func 38 constants map[string]*doc.Value 39 types map[string]*doc.Type 40 } 41 42 //nolint:goCheckNoGlobals // Ok. 43 var packages = make(map[string]*packageInfo) 44 45 func (pi *packageInfo) findFunc(name string) *doc.Func { 46 if pi.functions == nil { 47 addFunc := func(n string, f *doc.Func) { 48 pi.functions[n] = f 49 } 50 pi.functions = make(map[string]*doc.Func) 51 plainFunctions := append([]*doc.Func(nil), pi.docPkg.Funcs...) 52 53 for _, t := range pi.docPkg.Types { 54 plainFunctions = append(plainFunctions, t.Funcs...) 55 56 for _, f := range t.Methods { 57 addFunc(t.Name+"."+f.Name, f) 58 } 59 } 60 61 for _, f := range plainFunctions { 62 addFunc(f.Name, f) 63 } 64 } 65 66 return pi.functions[name] 67 } 68 69 func (pi *packageInfo) findConst(name string) *doc.Value { 70 if pi.constants == nil { 71 addConst := func(n string, c *doc.Value) { 72 pi.constants[n] = c 73 } 74 pi.constants = make(map[string]*doc.Value, len(pi.docPkg.Consts)) 75 76 for _, c := range pi.docPkg.Consts { 77 for _, n := range c.Names { 78 addConst(n, c) 79 } 80 } 81 } 82 83 return pi.constants[name] 84 } 85 86 func (pi *packageInfo) findType(name string) *doc.Type { 87 if pi.types == nil { 88 addType := func(n string, t *doc.Type) { 89 pi.types[n] = t 90 } 91 pi.types = make(map[string]*doc.Type, len(pi.docPkg.Types)) 92 93 for _, t := range pi.docPkg.Types { 94 addType(t.Name, t) 95 } 96 } 97 98 return pi.types[name] 99 } 100 101 // getInfoFunc looks up the documentation for a function. 102 func (pi *packageInfo) getInfoFunc(docFunc *doc.Func) (*docInfo, error) { 103 var dInfo *docInfo 104 105 dStart := pi.fSet.PositionFor(docFunc.Decl.Pos(), true) 106 dEnd := pi.fSet.PositionFor(docFunc.Decl.Body.Lbrace, true) 107 fEnd := pi.fSet.PositionFor(docFunc.Decl.End(), true) 108 decl, body, err := pi.snipFile( 109 dStart.Filename, dStart.Offset, dEnd.Offset, fEnd.Offset, 110 ) 111 112 if err == nil { 113 dInfo = &docInfo{ 114 header: decl, 115 body: body, 116 doc: strings.Split(strings.TrimSpace(docFunc.Doc), "\n"), 117 } 118 } 119 120 return dInfo, err 121 } 122 123 // getInfoConst looks up the documentation for a function. 124 func (pi *packageInfo) getInfoConst(docConst *doc.Value) (*docInfo, error) { 125 var dInfo *docInfo 126 127 dStart := pi.fSet.PositionFor(docConst.Decl.Pos(), true) 128 fEnd := pi.fSet.PositionFor(docConst.Decl.End(), true) 129 decl, body, err := pi.snipFile( 130 dStart.Filename, dStart.Offset, -1, fEnd.Offset, 131 ) 132 133 if err == nil { 134 dInfo = &docInfo{ 135 header: decl, 136 body: body, 137 doc: strings.Split(strings.TrimSpace(docConst.Doc), "\n"), 138 } 139 } 140 141 return dInfo, err 142 } 143 144 // getInfoType looks up the documentation for a function. 145 func (pi *packageInfo) getInfoType(docType *doc.Type) (*docInfo, error) { 146 var dInfo *docInfo 147 148 dStart := pi.fSet.PositionFor(docType.Decl.Pos(), true) 149 dEnd := pi.fSet.PositionFor(docType.Decl.Lparen, true) 150 fEnd := pi.fSet.PositionFor(docType.Decl.End(), true) 151 decl, body, err := pi.snipFile( 152 dStart.Filename, dStart.Offset, dEnd.Offset, fEnd.Offset, 153 ) 154 155 if err == nil { 156 dInfo = &docInfo{ 157 header: decl, 158 body: body, 159 doc: strings.Split(strings.TrimSpace(docType.Doc), "\n"), 160 } 161 } 162 163 return dInfo, err 164 } 165 166 // GetInfo looks up the documentation information for a declaration. 167 func (pi *packageInfo) getInfo(name string) (*docInfo, error) { 168 if verbose { 169 log.Printf("getInfo(%q)\n", name) 170 } 171 172 if name == pkgLabel { 173 // Return Package information. 174 return &docInfo{ 175 header: []string{pkgLabel + " " + pi.docPkg.Name}, 176 body: []string{pkgLabel + " " + pi.docPkg.Name}, 177 doc: strings.Split( 178 strings.TrimRight(pi.docPkg.Doc, "\n\t "), 179 "\n", 180 ), 181 }, nil 182 } 183 184 if f := pi.findFunc(name); f != nil { 185 // Process function 186 return pi.getInfoFunc(f) 187 } 188 189 if c := pi.findConst(name); c != nil { 190 // Process Constant 191 return pi.getInfoConst(c) 192 } 193 194 if t := pi.findType(name); t != nil { 195 // Process Type 196 return pi.getInfoType(t) 197 } 198 199 return nil, fmt.Errorf("%w: %s", ErrUnknownObject, name) 200 } 201 202 func leadingTabsToSpaces(lines []string) []string { 203 const fourSpaces = " " 204 205 for i, line := range lines { 206 newPrefix := "" 207 208 for j, mj := 0, len(line); j < mj; j++ { 209 if line[j] == '\t' { 210 newPrefix += fourSpaces 211 } else { 212 lines[i] = newPrefix + line[j:] 213 214 break 215 } 216 } 217 } 218 219 return lines 220 } 221 222 func (pi *packageInfo) snipFile( 223 fPath string, fPos, bPos, endPos int, 224 ) ([]string, []string, error) { 225 var ( 226 decl []string 227 body []string 228 err error 229 ) 230 231 d, err := os.ReadFile(fPath) //nolint:gosec // Ok. 232 233 if err == nil { 234 res := string(d) 235 236 switch { 237 case bPos < 0: 238 decl = nil 239 case bPos == 0: 240 decl = leadingTabsToSpaces(strings.Split(res[fPos:endPos], "\n")) 241 default: 242 decl = leadingTabsToSpaces(strings.Split( 243 res[fPos:bPos-1], 244 "\n", 245 )) 246 } 247 248 body = leadingTabsToSpaces(strings.Split(res[fPos:endPos], "\n")) 249 } 250 251 return decl, body, err //nolint:wrapcheck // Caller will wrap error. 252 } 253 254 func createPackageInfo(dir string) (*packageInfo, error) { 255 if verbose { 256 log.Print("Loading Package info for: ", dir) 257 } 258 259 pkgInfo := new(packageInfo) 260 pkgInfo.fSet = token.NewFileSet() 261 262 fileSet, err := parser.ParseDir(pkgInfo.fSet, dir, nil, 263 parser.ParseComments|parser.AllErrors, 264 ) 265 266 if err == nil { 267 for n, a := range fileSet { // Process the first non _test package. 268 if !strings.HasSuffix(n, "_test") { 269 pkgInfo.docPkg = doc.New( 270 a, n, doc.PreserveAST|doc.AllDecls|doc.AllMethods, 271 ) 272 273 return pkgInfo, nil 274 } 275 } 276 } 277 278 return nil, err //nolint:wrapcheck // Caller will wrap error. 279 } 280 281 func getInfo(dir, name string) (*docInfo, error) { 282 var ( 283 pkgInfo *packageInfo 284 dInfo *docInfo 285 ok bool 286 err error 287 ) 288 289 cwd, err := os.Getwd() 290 if err == nil { 291 pDir := filepath.Join(cwd, dir) 292 pkgInfo, ok = packages[pDir] 293 294 if !ok { 295 pkgInfo, err = createPackageInfo(dir) 296 if err == nil { 297 packages[pDir] = pkgInfo 298 } 299 } 300 } 301 302 if err == nil { 303 dInfo, err = pkgInfo.getInfo(name) 304 } 305 306 if err == nil { 307 return dInfo, nil 308 } 309 310 return nil, err 311 }