Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
susji committed Apr 23, 2024
0 parents commit ec18ae7
Show file tree
Hide file tree
Showing 17 changed files with 1,376 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Go

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.22.2'
- name: Lint
run: go vet ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
27 changes: 27 additions & 0 deletions .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: goreleaser
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22.2'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist
/typestringer
26 changes: 26 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
builds:
- id: typestringer
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
- openbsd
goarch:
- amd64
- arm64
- arm
mod_timestamp: "{{ .CommitTimestamp }}"
flags:
- -trimpath
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
checksum:
algorithm: sha256
name_template: "checksums.txt"
archives:
- id: typestringer
files:
- README.md
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# typestringer

This program can be used to generate customizable code based on Go type names.
One example would be the generation of `String()` functions to fulfill [the
Stringer interface](https://pkg.go.dev/fmt#Stringer).

# Usage

$ typestringer -help

Several options exist for customizing the generated code. `-include` and
`-ignore` may be used to define regular expressions for matching or not matching
specific types. Diagnostic output is written to standard error and generated
code by default to files located in the package paths. `-stdout` may be used to
redirect the generated code to standard output.

As usual, typestringer may also be executed with `go generate` by including a
comment such as the following somewhere in your code:

```go
//go:generate typestringer
```

# Installing

Builds for many platforms can be found
[here](https://github.com/susji/typestringer/releases). You may also use the Go
toolchain:

$ go install github.com/susji/typestringer@latest

# Examples

The examples below use the typestringer code.

## Basic

The type-specific format string is expanded with the type name passed two times
so escaping other formatting directives must be done with `%%`:

```shell
$ typestringer \
-stdout \
-preamble 'import fmt' \
-fmt 'func (t %s) String() string { return fmt.Sprintf("%s=%%v", t) }'
```

The generated code looks like this:

```go
// Automatically generated by typestringer with the following parameters:
// -stdout
// -preamble
// import fmt
// -fmt
// func (t %s) String() string { return fmt.Sprintf("%s=%%v", t) }
package main

import fmt

func (t stringsvar) String() string { return fmt.Sprintf("stringsvar=%v", t) }%
```

## Chaining

If specific types should be treated differently and one generated file is
desired, something like the following may be done:

```shell
$ { typestringer \
-stdout \
-preamble 'import fmt' \
-include '^FIRST$' \
-fmt 'func (t %s) String() string { return fmt.Sprintf("%s=%%v", t) }'$'\n' \
./generator/testdata/two;
typestringer \
-stdout \
-no-package \
-include '^SECOND$' \
-fmt 'func (t %s) String() string { return "%s" }'$'\n' \
./generator/testdata/two;
} 2>/dev/null
```

The generated code looks like this:

```go
// Automatically generated by typestringer with the following parameters:
// -stdout
// -preamble
// import fmt
// -include
// ^FIRST$
// -fmt
// func (t %s) String() string { return fmt.Sprintf("%s=%%v", t) }

// ./generator/testdata/two
package two

import fmt

func (t FIRST) String() string { return fmt.Sprintf("FIRST=%v", t) }
// Automatically generated by typestringer with the following parameters:
// -stdout
// -no-package
// -include
// ^SECOND$
// -fmt
// func (t %s) String() string { return "%s" }

// ./generator/testdata/two
func (t SECOND) String() string { return "SECOND" }
```
185 changes: 185 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package generator

import (
"errors"
"fmt"
"go/ast"
"go/token"
"io"
"os"
"path/filepath"
"regexp"

"golang.org/x/tools/go/packages"
)

// Generator contains the configuration for String() generation based on
// package's type names.
type Generator struct {
// Patterns passed to packages.Load.
Patterns []string
// List of regular expressions to determine which types are included.
// Empty list means to include all types by default.
Includes []*regexp.Regexp
// List of regular expressions to determine which types are ignored.
// Ignores takes precedence over Inludes.
Ignores []*regexp.Regexp
// Format string for writing out the type's String() receiver.
Format string
// Function used to create the output Writer for generated files. If
// left empty, the generated output is directed to a file in the target
// package with its filename determined by FormatFilename.
WriteCloserCreator WriteCloserCreator
// Determines where generated output is written. If set to nil,
// WriteCloserCreator is used.
Output io.WriteCloser
// Determines where Generator diagnostic output is written. If set to
// nil, os.Stderr will be used. If output should be discarded, something
// like io.Discard may be used.
DiagnosticOutput io.Writer
// If set true, the output stream will not be closed after package's
// code generation.
NoClose bool
// Format string for writing out the header of generated files. The
// format operand is the package name. May be left empty.
Header string
// String to write after the generated file's package has been declared
// and before the type-specific part begins. Useful for declaring things
// such as imports.
Preamble string
// If set true, generation will not output "package <name>".
NoPackage bool
}
type WriteCloserCreator func(filepath string, module string) (io.WriteCloser, error)

func (g *Generator) Generate() error {
if g.DiagnosticOutput == nil {
g.DiagnosticOutput = os.Stderr
}
if g.WriteCloserCreator == nil {
g.WriteCloserCreator = g.defaultwg
}
cfg := &packages.Config{
Mode: packages.NeedFiles | packages.NeedSyntax,
}
ps, err := packages.Load(cfg, g.Patterns...)
if err != nil {
return err
}
if len(ps) == 0 {
fmt.Fprintln(g.DiagnosticOutput, "no packages loaded")
return errors.New("no packages loaded")
}
var reterr error
for i, p := range ps {
fmt.Fprintln(g.DiagnosticOutput, "package with pattern", g.Patterns[i])
if len(p.Errors) > 0 {
fmt.Fprintln(g.DiagnosticOutput, "found package errors, not continuing")
for _, err := range p.Errors {
reterr = errors.Join(reterr, err)
fmt.Fprintln(g.DiagnosticOutput, err)
}
continue
}
if err := g.HandlePackage(p); err != nil {
fmt.Fprintln(g.DiagnosticOutput, "generate error:", err)
reterr = errors.Join(reterr, err)
}
}
return reterr
}

func (g *Generator) defaultwg(path, mod string) (io.WriteCloser, error) {
fn := filepath.Join(path, fmt.Sprintf(FormatFilename, mod))
w, err := os.Create(fn)
if err != nil {
fmt.Fprintln(g.DiagnosticOutput, err)
return nil, err
}
fmt.Fprintln(g.DiagnosticOutput, "writing file:", fn)
return w, nil
}

func (g *Generator) HandlePackage(p *packages.Package) error {
if len(p.GoFiles) == 0 {
return errors.New("no Go files in package")
}
typenames := []string{}
var packagename string
for _, a := range p.Syntax {
packagename = a.Name.Name
for _, decl := range a.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
if gd.Tok != token.TYPE {
continue
}
decl:
for _, sp := range gd.Specs {
ts := sp.(*ast.TypeSpec)
tn := ts.Name.Name
for _, r := range g.Ignores {
if r.MatchString(tn) {
fmt.Fprintln(g.DiagnosticOutput, "ignoring:", tn)
continue decl
}
}
if len(g.Includes) > 0 {
found := false
for _, r := range g.Includes {
if r.MatchString(tn) {
found = true
break
}
}
if !found {
fmt.Fprintln(g.DiagnosticOutput, "not included:", tn)
continue
}
}
fmt.Fprintln(g.DiagnosticOutput, "including:", tn)
typenames = append(typenames, ts.Name.Name)
}
}
}
var w io.WriteCloser
if g.Output != nil {
w = g.Output
} else {
var err error
w, err = g.WriteCloserCreator(filepath.Dir(p.GoFiles[0]), packagename)
if err != nil {
return err
}
if w == nil {
panic(errors.New("nil WriteCloser"))
}
}
if len(g.Header) > 0 {
fmt.Fprint(w, g.Header)
}
if !g.NoPackage {
fmt.Fprintf(w, "package %s\n\n", packagename)
}
if len(g.Preamble) > 0 {
fmt.Fprint(w, g.Preamble, "\n\n")
}
for _, tn := range typenames {
fmt.Fprintf(w, g.Format, tn, tn)
}
if !g.NoClose {
w.Close()
}
return nil
}

var (
// Format string for writing out the type-specific receiver. May of
// course be set to something completely different. The formatted
// operands are the type name passed twice.
FormatReceiver = "func (t %s) String() string { return \"%s\" }\n"
// Format string for determining the generated filenames.
FormatFilename = "%s_strings.go"
)
Loading

0 comments on commit ec18ae7

Please sign in to comment.