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  }