diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..73eb6c6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Request review from the repository owner for all changes. +* @markus621 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bea49eb..7270150 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.23.6' ] + go: [ '1.26' ] steps: - uses: actions/checkout@v3 diff --git a/.golangci.yml b/.golangci.yml index c564621..f2b16a7 100755 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,202 +1,61 @@ +version: "2" run: - go: "1.22" - concurrency: 4 + go: "1.26" timeout: 5m tests: false issues-exit-code: 1 modules-download-mode: readonly + allow-parallel-runners: true issues: - exclude-use-default: false - max-issues-per-linter: 100 - max-same-issues: 4 + max-issues-per-linter: 0 + max-same-issues: 0 new: false - exclude-files: - - ".+_test.go" - exclude-dirs: - - "vendor$" + fix: false output: formats: - - format: line-number - sort-results: true + text: + print-linter-name: true + print-issued-lines: true -linters-settings: - govet: - check-shadowing: true - enable: - - asmdecl - - assign - - atomic - - atomicalign - - bools - - buildtag - - cgocall - - composites - - copylocks - - deepequalerrors - - errorsas - - findcall - - framepointer - - httpresponse - - ifaceassert - - loopclosure - - lostcancel - - nilfunc - - nilness - - printf - - reflectvaluecompare - - shadow - - shift - - sigchanyzer - - sortslice - - stdmethods - - stringintconv - - structtag - - testinggoroutine - - tests - - unmarshal - - unreachable - - unsafeptr - - unusedresult - - unusedwrite - disable: - - fieldalignment - gofmt: - simplify: true - errcheck: - check-type-assertions: true - check-blank: true - gocyclo: - min-complexity: 30 - misspell: - locale: US - prealloc: - simple: true - range-loops: true - for-loops: true - unparam: - check-exported: false - gci: - skip-generated: true - custom-order: false - gosec: - includes: - - G101 # Look for hard coded credentials - - G102 # Bind to all interfaces - - G103 # Audit the use of unsafe block - - G104 # Audit errors not checked - - G106 # Audit the use of ssh.InsecureIgnoreHostKey - - G107 # Url provided to HTTP request as taint input - - G108 # Profiling endpoint automatically exposed on /debug/pprof - - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 - - G110 # Potential DoS vulnerability via decompression bomb - - G111 # Potential directory traversal - - G112 # Potential slowloris attack - - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) - - G114 # Use of net/http serve function that has no support for setting timeouts - - G201 # SQL query construction using format string - - G202 # SQL query construction using string concatenation - - G203 # Use of unescaped data in HTML templates - - G204 # Audit use of command execution - - G301 # Poor file permissions used when creating a directory - - G302 # Poor file permissions used with chmod - - G303 # Creating tempfile using a predictable path - - G304 # File path provided as taint input - - G305 # File traversal when extracting zip/tar archive - - G306 # Poor file permissions used when writing to a new file - - G307 # Deferring a method which returns an error - - G401 # Detect the usage of DES, RC4, MD5 or SHA1 - - G402 # Look for bad TLS connection settings - - G403 # Ensure minimum RSA key length of 2048 bits - - G404 # Insecure random number source (rand) - - G501 # Import blocklist: crypto/md5 - - G502 # Import blocklist: crypto/des - - G503 # Import blocklist: crypto/rc4 - - G504 # Import blocklist: net/http/cgi - - G505 # Import blocklist: crypto/sha1 - - G601 # Implicit memory aliasing of items from a range statement - excludes: - - G101 # Look for hard coded credentials - - G102 # Bind to all interfaces - - G103 # Audit the use of unsafe block - - G104 # Audit errors not checked - - G106 # Audit the use of ssh.InsecureIgnoreHostKey - - G107 # Url provided to HTTP request as taint input - - G108 # Profiling endpoint automatically exposed on /debug/pprof - - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 - - G110 # Potential DoS vulnerability via decompression bomb - - G111 # Potential directory traversal - - G112 # Potential slowloris attack - - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) - - G114 # Use of net/http serve function that has no support for setting timeouts - - G201 # SQL query construction using format string - - G202 # SQL query construction using string concatenation - - G203 # Use of unescaped data in HTML templates - - G204 # Audit use of command execution - - G301 # Poor file permissions used when creating a directory - - G302 # Poor file permissions used with chmod - - G303 # Creating tempfile using a predictable path - - G304 # File path provided as taint input - - G305 # File traversal when extracting zip/tar archive - - G306 # Poor file permissions used when writing to a new file - - G307 # Deferring a method which returns an error - - G401 # Detect the usage of DES, RC4, MD5 or SHA1 - - G402 # Look for bad TLS connection settings - - G403 # Ensure minimum RSA key length of 2048 bits - - G404 # Insecure random number source (rand) - - G501 # Import blocklist: crypto/md5 - - G502 # Import blocklist: crypto/des - - G503 # Import blocklist: crypto/rc4 - - G504 # Import blocklist: net/http/cgi - - G505 # Import blocklist: crypto/sha1 - - G601 # Implicit memory aliasing of items from a range statement - exclude-generated: true - severity: medium - confidence: medium - concurrency: 12 - config: - global: - nosec: true - "#nosec": "#my-custom-nosec" - show-ignored: true - audit: true - G101: - pattern: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" - ignore_entropy: false - entropy_threshold: "80.0" - per_char_threshold: "3.0" - truncate: "32" - G104: - fmt: - - Fscanf - G111: - pattern: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" - G301: "0750" - G302: "0600" - G306: "0600" - - lll: - line-length: 130 - tab-width: 1 - staticcheck: - go: "1.15" - # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks - # Default: ["*"] - checks: [ "*", "-SA1019" ] +formatters: + exclusions: + paths: + - vendors/ + enable: + - gofmt + - goimports linters: - disable-all: true + settings: + staticcheck: + checks: + - all + - -S1023 + - -ST1000 + - -ST1003 + - -ST1020 + gosec: + excludes: + - G104 + - G115 + - G301 + - G304 + - G306 + - G501 + - G505 + exclusions: + paths: + - vendors/ + default: none enable: - govet - - gofmt - errcheck - misspell - gocyclo - ineffassign - - goimports - - nakedret - unparam - unused - prealloc @@ -207,5 +66,3 @@ linters: - errorlint - bodyclose - gosec - - lll - fast: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..799a38d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Agent instructions + +## Project map + +- `golang/` contains the Go-language fluent API, Go formatter, and Go syntax policy in `golang/config.go`. +- `typescript/` contains the TypeScript and TSX fluent API and language policy. +- `python/` contains the Python 3.10+ fluent API, built-in type builders, and language policy. +- `types/` defines the public `Token` interface. +- `internal/gen/` walks token values and applies language-neutral layout rules to semantic token styles. +- `internal/models/` implements reusable token renderers. `internal/config/` defines the language policy contract; language-specific rules belong in the corresponding language package, never as literals or branches in `internal/`. +- `README.md` links the language-specific API guides in `golang/`, `typescript/`, and `python/`. Keep examples consistent with each adapter’s exported constructors. + +## Working in this repository + +- Work from the repository root. The module path is `go.osspkg.com/gogen` and `go.mod` requires Go 1.26. +- Preserve the boundary between language packages and reusable `internal` packages. Keep keyword inventories, identifier rules, literal quoting, operator classification, and comment styles in the language package config. +- Extend `internal/config.Config` only with language-neutral capabilities needed by multiple language adapters. Do not add branches for Go (or another target language) to `internal/gen` or `internal/models`. +- Token implementations satisfy `types.Token` by rendering to an `io.Writer`; propagate writer and render errors. +- `golang.Render` formats output by default. `SetRawMode` and `SetDefaultMode` change package-wide Go rendering behavior; account for that shared state when changing Go rendering code. TypeScript and Python render readable source directly, without formatters. Python targets 3.10+ and indents suites with four spaces. +- Each adapter’s `ID` and `Op` validate against its own language rules. Update focused examples or tests when changing validation or exported constructors. + +## Persistent project memory + +Use the Chroma collection `chat_gogen_memory` for durable repository context. + +- Before querying, adding, updating, or deleting memory, ensure the collection exists. Call `chroma_list_collections`; if it is missing, create exactly `chat_gogen_memory` with the default embedding configuration. Do not ask for permission to create it. +- For non-trivial implementation, debugging, architecture, API, or infrastructure work, query the collection with a concise semantic description of the task before making important decisions. +- If a query fails because the collection is missing, list collections, create it if needed, and retry once. For other memory failures, continue from repository evidence and do not invent retrieved information. +- After non-trivial work, store durable decisions or lessons that are likely to help future work. Query for related entries first; update an existing entry when refining a decision and add a new one only when it is distinct. +- Keep one concise, self-contained fact per document. Do not store transcripts, routine command output, facts directly recoverable from source or docs, speculation, or secrets. +- Treat memory as supplemental. Explicit instructions, current source and tests, and current documentation take precedence over it. + +## Validation commands + +Run these from the repository root: + +- `make tests` runs the repository test task through `goppy`. +- `make lint` runs the configured lint task through `goppy`. +- `make build` runs the configured amd64 build through `goppy`. +- `make ci` is the GitHub Actions workflow and runs the pre-commit targets, including license, lint, tests, and build. + +`make ci` also runs `make install`, which installs `goppy@latest` and invokes `goppy setup-lib`. This may require network access and affect local setup; inspect `git status` after running it. CI is configured for Go 1.26 in `.github/workflows/ci.yml`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..69220ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing + +## Prerequisites + +- Go 1.26 or newer +- `goppy` for the repository's Makefile tasks + +## Checks + +Run checks from the repository root: + +```sh +make tests +make lint +make build +``` + +Before opening a pull request, run the relevant checks and include the user-visible behavior and validation results in the description. Keep changes focused and add regression tests for behavior changes. + +`make ci` runs the same workflow used by GitHub Actions. It also installs `goppy@latest` and invokes `goppy setup-lib`, which may require network access and modify local setup. diff --git a/Makefile b/Makefile index 03d4159..e532830 100755 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL=/bin/bash .PHONY: install install: - go install go.osspkg.com/goppy/v2/cmd/goppy@latest + go install go.osspkg.com/goppy/v3/cmd/goppy@latest goppy setup-lib .PHONY: lint @@ -24,8 +24,8 @@ tests: goppy test .PHONY: pre-commit -pre-commit: install license lint tests build +pre-commit: license lint tests build .PHONY: ci -ci: pre-commit +ci: install pre-commit diff --git a/README.md b/README.md index f9500cb..dc90ca5 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# gogen \ No newline at end of file +# gogen + +[![Go Version](https://img.shields.io/github/go-mod/go-version/osspkg/gogen)](https://go.dev/) [![License](https://img.shields.io/github/license/osspkg/gogen)](LICENSE) [![CI](https://img.shields.io/github/actions/workflow/status/osspkg/gogen/ci.yml?branch=master&label=CI)](https://github.com/osspkg/gogen/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/go.osspkg.com/gogen/golang.svg)](https://pkg.go.dev/go.osspkg.com/gogen/golang) [![TypeScript Reference](https://pkg.go.dev/badge/go.osspkg.com/gogen/typescript.svg)](https://pkg.go.dev/go.osspkg.com/gogen/typescript) [![Python Reference](https://pkg.go.dev/badge/go.osspkg.com/gogen/python.svg)](https://pkg.go.dev/go.osspkg.com/gogen/python) + +`gogen` is a token-based source generation library for Go, TypeScript, TSX, and Python. Language adapters provide composable builders while shared internals handle reusable token rendering and layout. + +## Demo + +Generate a formatted Go source file: + +```go +package main + +import ( + "bytes" + "fmt" + + gogen "go.osspkg.com/gogen/golang" +) + +func main() { + file := gogen.Package("main").Import("fmt", "fmt").Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Pkg("fmt").ID("Println").Call(gogen.Text("hello, gogen")), + ), + ) + + var source bytes.Buffer + if err := gogen.Render(&source, file); err != nil { + panic(err) + } + fmt.Print(source.String()) +} +``` + +## Getting started + +Requires Go 1.26 or newer: + +```sh +go get go.osspkg.com/gogen/golang go.osspkg.com/gogen/typescript go.osspkg.com/gogen/python +``` + +## Language guides + +| Language | Package | Guide | +| --- | --- | --- | +| Go | [`go.osspkg.com/gogen/golang`](https://pkg.go.dev/go.osspkg.com/gogen/golang) | [Go builder guide](golang/README.md) | +| TypeScript and TSX | [`go.osspkg.com/gogen/typescript`](https://pkg.go.dev/go.osspkg.com/gogen/typescript) | [TypeScript and TSX guide](typescript/README.md) | +| Python 3.10+ | [`go.osspkg.com/gogen/python`](https://pkg.go.dev/go.osspkg.com/gogen/python) | [Python guide](python/README.md) | + +The Go adapter applies `go/format` by default. The TypeScript and Python adapters write readable source without invoking a formatter; use the language toolchain or a formatter separately when needed. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and local checks. + +## Contributors + +[![Contributors](https://img.shields.io/github/contributors/osspkg/gogen)](https://github.com/osspkg/gogen/graphs/contributors) + +## License + +This project is licensed under the [BSD 3-Clause License](LICENSE). diff --git a/go.mod b/go.mod index 035c4a9..d7427c5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module go.osspkg.com/gogen -go 1.23.6 +go 1.26 diff --git a/golang/README.md b/golang/README.md new file mode 100644 index 0000000..d563746 --- /dev/null +++ b/golang/README.md @@ -0,0 +1,138 @@ +# Go code builder + +The `go.osspkg.com/gogen/golang` package builds Go source from composable tokens. Package-level constructors start token sequences; fluent `*Tokens` methods append declarations, expressions, and statements. + +```sh +go get go.osspkg.com/gogen/golang +``` + +## Build and render a Go file + +The following builds a small source file with grouped imports and type declarations: + +```go +package main + +import ( + "bytes" + "fmt" + + gogen "go.osspkg.com/gogen/golang" +) + +func main() { + file := gogen.Package("main"). + ImportBlock( + gogen.Text("fmt"), + gogen.ID("json").Text("encoding/json"), + ). + TypeBlock( + gogen.ID("Name").String(), + gogen.ID("Count").Int(), + ). + Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Pkg("fmt").ID("Println").Call(gogen.Text("generated")), + ), + ) + + var source bytes.Buffer + if err := gogen.Render(&source, file); err != nil { + panic(err) + } + fmt.Print(source.String()) +} +``` + +Output: + +```go +package main + +import ( + json "encoding/json" + "fmt" +) + +type ( + Name string + Count int +) + +func main() { + fmt.Println("generated") +} +``` + +The `ImportBlock` specs are import paths, optionally preceded by an alias. `TypeBlock` specs contain a type name followed by its underlying type. Both builders add the grouping syntax and layout. + +## Struct fields and tags + +`Field(name, type, tags...)` adds a named struct field. Tag arguments are alternating key/value strings; the builder quotes and escapes values for a Go struct tag: + +```go +user := gogen.Package("main").Join( + gogen.Type().ID("User").Struct().Block( + gogen.Field("ID", gogen.Uint64(), "json", "id,omitempty", "db", "user_id"), + gogen.Field("Name", gogen.String(), "json", "name"), + ), +) +``` + +This generates: + +```go +package main + +type User struct { + ID uint64 `json:"id,omitempty" db:"user_id"` + Name string `json:"name"` +} +``` + +An odd number of tag arguments causes rendering to return an error. + +## Expressions and generic calls + +Use `Text` for escaped string literals, `ID` for identifiers, `Index` for indexed access, and `TypeArgs` for generic type arguments: + +```go +expression := gogen.ID("lookup").TypeArgs(gogen.String()).Call( + gogen.ID("items").Index(gogen.Raw("0")), +) +``` + +Output: + +```go +lookup[string](items[0]) +``` + +`Call` adds call parentheses, `Bracket` adds parentheses to a sequence, and `List` creates a comma-separated sequence without delimiters. `KeyValue` creates a keyed element in a composite literal. + +## Builder reference + +Most builders are available both as package-level constructors and as chainable `*Tokens` methods. A few are intentionally available in only one form; those are noted below. + +| Area | Builders | Notes | +| --- | --- | --- | +| File and declarations | `Package`, `Import`, `ImportBlock`, `Type`, `TypeBlock`, `Var`, `Const` | Start a package, add imports, and declare values or types. Group declaration specs omit the `import` or `type` keyword. | +| Functions and statements | `Func`, `Return`, `Defer`, `Go`, `If`, `Else`, `ElseIf`, `For`, `Range`, `Switch`, `Select`, `Case`, `Default`, `Break`, `Continue`, `Fallthrough`, `Goto` | Compose functions and control flow. `Defer`, `Go`, `Switch`, and `Select` are package-level only; `Range`, `Else`, `ElseIf`, `Break`, `Continue`, `Fallthrough`, and `Goto` are fluent-only. | +| Calls and lists | `Call`, `Bracket`, `Params`, `List` | Build calls, parenthesized expressions, parameter lists, or comma-separated values. `Params` is package-level only. | +| Expressions | `ID`, `Pkg`, `Op`, `Raw`, `Text`, `Index`, `TypeArgs`, `KeyValue` | Build identifiers, package selectors, operators, literals, indexes, generic arguments, and keyed values. | +| Composite values and allocation | `Struct`, `Field`, `New`, `Make`, `Append` | Build structs, tagged fields, and common `new`, `make`, and `append` expressions. | +| Types | `Any`, `Chan`, `Interface`, `Slice`, `Array`, `Map`, `Bool`, `Byte`, `Rune`, `String`, `Error`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`, `Uintptr`, `Float32`, `Float64`, `Complex64`, `Complex128`, `Nil` | Add predeclared types and common Go type forms. | +| Layout and output | `Block`, `Comment`, `Line`, `Join`, `Render`, `Unwrap` | Build indented blocks, add comments or line breaks, combine tokens, render, or access the token slice. | +| Package render mode | `Render`, `SetRawMode`, `SetDefaultMode` | `Render` applies `go/format` by default. The mode setters affect subsequent package-level render calls. | + +## Rendering modes and errors + +`Tokens.Render(w)` writes the readable token layout without formatting. Package-level `golang.Render(w, token)` applies `go/format` by default. `SetRawMode()` disables formatting for subsequent package-level calls; `SetDefaultMode()` enables it again. These mode setters change package-wide behavior, so prefer `Tokens.Render` when a call should explicitly skip formatting without changing that behavior. + +Rendering returns errors from tokens or the destination writer. With formatting enabled, `Render` also returns errors from `go/format` when the generated text is not valid Go syntax. Formatting does not type-check the generated program. `ID` validates identifier syntax, and `Op` rejects operators unsupported by this adapter. Rendering a `Field` returns an error if its tag arguments do not form key/value pairs. + +`Raw` writes source text verbatim and does not validate it. Use it only for source controlled by the caller. `Array(n)` emits the supplied array length; it does not check whether that length is a legal Go constant expression. + +To provide a custom token, implement [`types.Token`](../types/token.go) and write its source in `Render(io.Writer) error`. + +For the repository overview and other language guides, see the [root README](../README.md), [TypeScript/TSX guide](../typescript/README.md), and [Python guide](../python/README.md). The Go package reference is available on [pkg.go.dev](https://pkg.go.dev/go.osspkg.com/gogen/golang). diff --git a/golang/config.go b/golang/config.go index 7072110..6dc8c87 100644 --- a/golang/config.go +++ b/golang/config.go @@ -6,14 +6,25 @@ package golang import ( + "errors" + "regexp" + "strconv" + "strings" + cfg "go.osspkg.com/gogen/internal/config" + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" ) -var _ cfg.Config = (*config)(nil) +var _ cfg.Config = config{} type config struct{} -func (c config) OperationAvailable(op string) bool { +var errInvalidStructTag = errors.New("invalid struct tags: expected key/value pairs") + +var identifierPattern = regexp.MustCompile(`(?i)^[a-z][0-9a-z_]*$`) + +func (config) OperationAvailable(op string) bool { switch op { case "+", "-", "*", "/", "%", "&", "|", "^", "<<", ">>", "&^", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", "&^=", "&&", "||", "<-", "++", "--", "==", "<", ">", "=", "!", "~", "!=", @@ -24,16 +35,125 @@ func (c config) OperationAvailable(op string) bool { } } -func (config) CommentSingle() cfg.OpenClose { - return cfg.OpenClose{ - Open: "//", - Close: "\n", +func (config) OperationKind(op string) cfg.TokenKind { + switch op { + case ".": + return cfg.TokenDot + case ",": + return cfg.TokenComma + case ":": + return cfg.TokenColon + case ";": + return cfg.TokenSemicolon + case "(": + return cfg.TokenOpenParen + case ")": + return cfg.TokenCloseParen + case "[": + return cfg.TokenOpenSquare + case "]": + return cfg.TokenCloseSquare + case "{": + return cfg.TokenBlockOpen + case "}": + return cfg.TokenBlockClose + case "++", "--", "...": + return cfg.TokenPostfixOperator + case "+", "-", "*", "&", "!", "^", "~", "<-": + return cfg.TokenPrefixOperator + default: + return cfg.TokenDefault } } +func (config) RawKind(text string, verified bool) cfg.TokenKind { + if verified { + return cfg.TokenDefault + } + switch { + case text == "]": + return cfg.TokenCloseSquare + case text == "map[" || strings.HasPrefix(text, "["): + return cfg.TokenTypePrefix + default: + return cfg.TokenDefault + } +} + +func (config) IsIdentifier(text string) bool { + return identifierPattern.MatchString(text) +} + +func (config) CanEndExpression(word string) bool { + switch strings.TrimSpace(word) { + case "break", "case", "const", "continue", "defer", "else", "fallthrough", "for", "func", "go", "goto", "if", "import", "package", "return", "select", "switch", "type", "var": + return false + default: + return true + } +} + +func (config) QuoteString(value string) string { + return strconv.Quote(value) +} + +func (config) CommentSingle() cfg.OpenClose { + return cfg.OpenClose{Open: "//", Close: "\n", SpaceAfterOpenWhenNeeded: true} +} + func (config) CommentMulti() cfg.OpenClose { - return cfg.OpenClose{ - Open: "/*\n", - Close: "\n*/\n", + return cfg.OpenClose{Open: "/*\n", Close: "\n*/\n"} +} + +func (config) structTag(tags []string) (string, error) { + if len(tags)%2 != 0 { + return "", errInvalidStructTag + } + if len(tags) == 0 { + return "", nil + } + + var tag strings.Builder + for i := 0; i < len(tags); i += 2 { + if i > 0 { + tag.WriteByte(' ') + } + tag.WriteString(tags[i]) + tag.WriteByte(':') + tag.WriteString(strconv.Quote(tags[i+1])) } + + value := tag.String() + if strings.ContainsAny(value, "`\r\n") { + return strconv.Quote(value), nil + } + return "`" + value + "`", nil +} + +func keyword(value string) *models.Keyword[config] { + return &models.Keyword[config]{C: config{}, D: value} +} + +func identifier(value string) *models.Keyword[config] { + return &models.Keyword[config]{C: config{}, D: value, Verify: true} +} + +func rawKeyword(value string) *models.Keyword[config] { + return &models.Keyword[config]{C: config{}, D: value, Raw: true} +} + +func rawToken(value string) *models.Raw[config] { + return &models.Raw[config]{C: config{}, D: value} +} + +func verifiedRawToken(value string) *models.Raw[config] { + return &models.Raw[config]{C: config{}, D: value, Verify: true} +} + +func rawTokenOf(token types.Token) *models.Raw[config] { + return &models.Raw[config]{C: config{}, T: token} +} + +func textToken(value string) *models.Text[config] { + return &models.Text[config]{C: config{}, D: value} } diff --git a/golang/doc.go b/golang/doc.go new file mode 100644 index 0000000..9230190 --- /dev/null +++ b/golang/doc.go @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package golang builds Go source from composable tokens. Package-level +// constructors create token sequences, and fluent methods append declarations, +// expressions, and statements to them. +// +// Render applies go/format by default. Tokens.Render writes the readable token +// layout without formatting, and SetRawMode disables formatting for subsequent +// package-level Render calls. Raw tokens are always emitted verbatim. +package golang diff --git a/golang/example_test.go b/golang/example_test.go new file mode 100644 index 0000000..cd20a82 --- /dev/null +++ b/golang/example_test.go @@ -0,0 +1,89 @@ +package golang_test + +import ( + "bytes" + "fmt" + + gogen "go.osspkg.com/gogen/golang" +) + +func ExampleField() { + file := gogen.Package("main").Join( + gogen.Type().ID("User").Struct().Block( + gogen.Field("ID", gogen.Uint64(), "json", "id,omitempty", "db", "user_id"), + gogen.Field("Name", gogen.String(), "json", "name"), + ), + ) + + var source bytes.Buffer + if err := gogen.Render(&source, file); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // package main + // + // type User struct { + // ID uint64 `json:"id,omitempty" db:"user_id"` + // Name string `json:"name"` + // } +} + +func ExampleImportBlock() { + file := gogen.Package("main"). + ImportBlock( + gogen.Text("fmt"), + gogen.ID("json").Text("encoding/json"), + ). + TypeBlock( + gogen.ID("Name").String(), + gogen.ID("Count").Int(), + ). + Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Pkg("fmt").ID("Println").Call(gogen.Text("generated")), + ), + ) + + var source bytes.Buffer + if err := gogen.Render(&source, file); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // package main + // + // import ( + // json "encoding/json" + // "fmt" + // ) + // + // type ( + // Name string + // Count int + // ) + // + // func main() { + // fmt.Println("generated") + // } +} + +func Example() { + expression := gogen.ID("lookup").TypeArgs(gogen.String()).Call( + gogen.ID("items").Index(gogen.Raw("0")), + ) + + var source bytes.Buffer + if err := expression.Render(&source); err != nil { + fmt.Println(err) + return + } + fmt.Println(source.String()) + + // Output: + // lookup[string](items[0]) +} diff --git a/golang/render.go b/golang/render.go index e86eb74..0fe81b7 100644 --- a/golang/render.go +++ b/golang/render.go @@ -20,14 +20,17 @@ import ( var rawMode = atomic.Bool{} +// SetRawMode disables go/format for subsequent Render calls. func SetRawMode() { rawMode.Store(true) } +// SetDefaultMode enables go/format for subsequent Render calls. func SetDefaultMode() { rawMode.Store(false) } +// Render writes arg to w and formats the generated source with go/format unless raw mode is enabled. It returns render, formatting, or writer errors. func Render(w io.Writer, arg types.Token) error { buf := bytes.NewBuffer(nil) diff --git a/golang/render_test.go b/golang/render_test.go new file mode 100644 index 0000000..eaa4af5 --- /dev/null +++ b/golang/render_test.go @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package golang_test + +import ( + "bytes" + "io" + "testing" + + gogen "go.osspkg.com/gogen/golang" +) + +func TestTokensRenderSpacing(t *testing.T) { + tests := []struct { + name string + token interface{ Render(io.Writer) error } + want string + }{ + { + name: "declaration and assignment", + token: gogen.Var().ID("count").Op("=").Raw("1"), + want: "var count = 1", + }, + { + name: "binary operator", + token: gogen.ID("left").Op("+").ID("right"), + want: "left + right", + }, + { + name: "unary operator", + token: gogen.Op("*").ID("value"), + want: "*value", + }, + { + name: "postfix increment", + token: gogen.ID("index").Op("++"), + want: "index++", + }, + { + name: "variadic expansion", + token: gogen.Slice().Byte().Op("..."), + want: "[]byte...", + }, + { + name: "raw content is preserved", + token: gogen.Raw("alpha + beta\n gamma"), + want: "alpha + beta\n gamma", + }, + { + name: "selector and call", + token: gogen.Pkg("fmt").ID("Println").Call(gogen.Text("hello")), + want: "fmt.Println(\"hello\")", + }, + { + name: "call argument separators", + token: gogen.ID("combine").Call(gogen.ID("left"), gogen.ID("right")), + want: "combine(left, right)", + }, + { + name: "index expression", + token: gogen.ID("items").Index(gogen.ID("index")), + want: "items[index]", + }, + { + name: "generic type arguments", + token: gogen.ID("Map").TypeArgs(gogen.ID("string"), gogen.ID("int")), + want: "Map[string, int]", + }, + { + name: "keyed element", + token: gogen.KeyValue(gogen.Text("name"), gogen.ID("value")), + want: `"name": value`, + }, + { + name: "empty function block", + token: gogen.Func().ID("run").Bracket().Block(), + want: "func run() {}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got bytes.Buffer + if err := tt.token.Render(&got); err != nil { + t.Fatal(err) + } + if got.String() != tt.want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), tt.want) + } + }) + } +} + +func TestRenderFormatsCompositeExpressions(t *testing.T) { + token := gogen.Package("main").Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Var().ID("value").Op("="). + Map(gogen.String(), gogen.Int()).Op("{"). + KeyValue(gogen.Text("key"), gogen.ID("items").Index(gogen.Raw("0"))). + Op("}"), + ), + ) + + var got bytes.Buffer + if err := gogen.Render(&got, token); err != nil { + t.Fatal(err) + } + + want := "package main\n\nfunc main() {\n\tvar value = map[string]int{\"key\": items[0]}\n}\n" + if got.String() != want { + t.Errorf("formatted source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderFileAndNestedBlocks(t *testing.T) { + file := gogen.Package("main"). + Import("fmt", "fmt"). + Join( + gogen.Func().ID("main").Bracket().Block( + gogen.If().ID("ready").Block( + gogen.Pkg("fmt").ID("Println").Call(gogen.Text("ready")), + ).Else().Block( + gogen.Pkg("fmt").ID("Println").Call(gogen.Text("waiting")), + ), + ), + ) + + var got bytes.Buffer + if err := file.Render(&got); err != nil { + t.Fatal(err) + } + + want := "package main\nimport fmt \"fmt\"\nfunc main() {\n\tif ready {\n\t\tfmt.Println(\"ready\")\n\t} else {\n\t\tfmt.Println(\"waiting\")\n\t}\n}" + if got.String() != want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderDeclarationBlocks(t *testing.T) { + file := gogen.Package("main"). + ImportBlock( + gogen.Text("fmt"), + gogen.ID("json").Text("encoding/json"), + ). + TypeBlock( + gogen.ID("Name").String(), + gogen.ID("Count").Int(), + ) + + var got bytes.Buffer + if err := file.Render(&got); err != nil { + t.Fatal(err) + } + + want := "package main\nimport (\n\t\"fmt\"\n\tjson \"encoding/json\"\n)\ntype (\n\tName string\n\tCount int\n)\n" + if got.String() != want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestRenderFormatsDeclarationBlocks(t *testing.T) { + file := gogen.Package("main"). + ImportBlock(gogen.Text("fmt"), gogen.ID("json").Text("encoding/json")). + TypeBlock(gogen.ID("Name").String(), gogen.ID("Count").Int()) + + var got bytes.Buffer + if err := gogen.Render(&got, file); err != nil { + t.Fatal(err) + } + + want := "package main\n\nimport (\n\tjson \"encoding/json\"\n\t\"fmt\"\n)\n\ntype (\n\tName string\n\tCount int\n)\n" + if got.String() != want { + t.Errorf("formatted source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderStructFieldTags(t *testing.T) { + token := gogen.Struct().Block( + gogen.Field("ID", gogen.Uint64(), "json", "id,omitempty", "db", "user_id"), + gogen.Field("Name", gogen.String()), + ) + + var got bytes.Buffer + if err := token.Render(&got); err != nil { + t.Fatal(err) + } + + want := "struct {\n\tID uint64 `json:\"id,omitempty\" db:\"user_id\"`\n\tName string\n}" + if got.String() != want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestRenderFormatsStructFieldTags(t *testing.T) { + token := gogen.Package("main").Join( + gogen.Type().ID("User").Struct().Block( + gogen.Field("ID", gogen.Uint64(), "json", "id,omitempty", "db", "user_id"), + gogen.Field("Name", gogen.String(), "json", "name"), + ), + ) + + var got bytes.Buffer + if err := gogen.Render(&got, token); err != nil { + t.Fatal(err) + } + + want := "package main\n\ntype User struct {\n\tID uint64 `json:\"id,omitempty\" db:\"user_id\"`\n\tName string `json:\"name\"`\n}\n" + if got.String() != want { + t.Errorf("formatted source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderStructFieldRejectsUnpairedTag(t *testing.T) { + var got bytes.Buffer + if err := gogen.Field("ID", gogen.Uint64(), "json").Render(&got); err == nil { + t.Fatal("Render() accepted an unpaired struct tag key") + } +} + +func TestTokensRenderPreservesMultilineRawInBlock(t *testing.T) { + token := gogen.Block(gogen.Raw("first\n second")) + + var got bytes.Buffer + if err := token.Render(&got); err != nil { + t.Fatal(err) + } + + want := "{\n\tfirst\n second\n}" + if got.String() != want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderCommentInBlock(t *testing.T) { + token := gogen.Func().ID("run").Bracket().Block( + gogen.Comment("marker"), + gogen.Return(), + ) + + var got bytes.Buffer + if err := token.Render(&got); err != nil { + t.Fatal(err) + } + + want := "func run() {\n\t// marker\n\treturn\n}" + if got.String() != want { + t.Errorf("rendered source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestRenderFormatsTokenOutput(t *testing.T) { + var got bytes.Buffer + token := gogen.Package("main").Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Var().ID("count").Op("=").Raw("1"), + ), + ) + + if err := gogen.Render(&got, token); err != nil { + t.Fatal(err) + } + + want := "package main\n\nfunc main() {\n\tvar count = 1\n}\n" + if got.String() != want { + t.Errorf("formatted source:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestTokensRenderRejectsInvalidOperator(t *testing.T) { + var got bytes.Buffer + if err := gogen.ID("value").Op("??").Render(&got); err == nil { + t.Fatal("Render() accepted an invalid operator") + } +} diff --git a/golang/token_basic_types.go b/golang/token_basic_types.go index 2709c8c..97444fe 100644 --- a/golang/token_basic_types.go +++ b/golang/token_basic_types.go @@ -6,244 +6,295 @@ package golang import ( - "go.osspkg.com/gogen/internal/models" "go.osspkg.com/gogen/types" ) +// Struct appends the struct type keyword to the token sequence. func (v *Tokens) Struct() *Tokens { - *v = append(*v, &models.Keyword{D: "struct"}) + *v = append(*v, keyword("struct")) return v } +// Struct creates a token sequence containing the struct type keyword. func Struct() *Tokens { return create().Struct() } +// Interface appends the interface type keyword to the token sequence. func (v *Tokens) Interface() *Tokens { - *v = append(*v, &models.Keyword{D: "interface"}) + *v = append(*v, keyword("interface")) return v } +// Interface creates a token sequence containing the interface type keyword. func Interface() *Tokens { return create().Interface() } +// Any appends the predeclared any type to the token sequence. func (v *Tokens) Any() *Tokens { - *v = append(*v, &models.Keyword{D: "any"}) + *v = append(*v, keyword("any")) return v } +// Any creates a token sequence containing the predeclared any type. func Any() *Tokens { return create().Any() } +// Nil appends the predeclared nil value to the token sequence. func (v *Tokens) Nil() *Tokens { - *v = append(*v, &models.Keyword{D: "nil"}) + *v = append(*v, keyword("nil")) return v } +// Nil creates a token sequence containing the predeclared nil value. func Nil() *Tokens { return create().Nil() } +// Chan appends the chan type keyword to the token sequence. func (v *Tokens) Chan() *Tokens { - *v = append(*v, &models.Keyword{D: "chan"}) + *v = append(*v, keyword("chan")) return v } +// Chan creates a token sequence containing the chan type keyword. func Chan() *Tokens { return create().Chan() } +// Uint8 appends the predeclared uint8 type to the token sequence. func (v *Tokens) Uint8() *Tokens { - *v = append(*v, &models.Keyword{D: "uint8"}) + *v = append(*v, keyword("uint8")) return v } +// Uint8 creates a token sequence containing the predeclared uint8 type. func Uint8() *Tokens { return create().Uint8() } +// Uint16 appends the predeclared uint16 type to the token sequence. func (v *Tokens) Uint16() *Tokens { - *v = append(*v, &models.Keyword{D: "uint16"}) + *v = append(*v, keyword("uint16")) return v } +// Uint16 creates a token sequence containing the predeclared uint16 type. func Uint16() *Tokens { return create().Uint16() } +// Uint32 appends the predeclared uint32 type to the token sequence. func (v *Tokens) Uint32() *Tokens { - *v = append(*v, &models.Keyword{D: "uint32"}) + *v = append(*v, keyword("uint32")) return v } +// Uint32 creates a token sequence containing the predeclared uint32 type. func Uint32() *Tokens { return create().Uint32() } +// Uint64 appends the predeclared uint64 type to the token sequence. func (v *Tokens) Uint64() *Tokens { - *v = append(*v, &models.Keyword{D: "uint64"}) + *v = append(*v, keyword("uint64")) return v } +// Uint64 creates a token sequence containing the predeclared uint64 type. func Uint64() *Tokens { return create().Uint64() } +// Int8 appends the predeclared int8 type to the token sequence. func (v *Tokens) Int8() *Tokens { - *v = append(*v, &models.Keyword{D: "int8"}) + *v = append(*v, keyword("int8")) return v } +// Int8 creates a token sequence containing the predeclared int8 type. func Int8() *Tokens { return create().Int8() } +// Int16 appends the predeclared int16 type to the token sequence. func (v *Tokens) Int16() *Tokens { - *v = append(*v, &models.Keyword{D: "int16"}) + *v = append(*v, keyword("int16")) return v } +// Int16 creates a token sequence containing the predeclared int16 type. func Int16() *Tokens { return create().Int16() } +// Int32 appends the predeclared int32 type to the token sequence. func (v *Tokens) Int32() *Tokens { - *v = append(*v, &models.Keyword{D: "int32"}) + *v = append(*v, keyword("int32")) return v } +// Int32 creates a token sequence containing the predeclared int32 type. func Int32() *Tokens { return create().Int32() } +// Int64 appends the predeclared int64 type to the token sequence. func (v *Tokens) Int64() *Tokens { - *v = append(*v, &models.Keyword{D: "int64"}) + *v = append(*v, keyword("int64")) return v } +// Int64 creates a token sequence containing the predeclared int64 type. func Int64() *Tokens { return create().Int64() } +// Float32 appends the predeclared float32 type to the token sequence. func (v *Tokens) Float32() *Tokens { - *v = append(*v, &models.Keyword{D: "float32"}) + *v = append(*v, keyword("float32")) return v } +// Float32 creates a token sequence containing the predeclared float32 type. func Float32() *Tokens { return create().Float32() } +// Float64 appends the predeclared float64 type to the token sequence. func (v *Tokens) Float64() *Tokens { - *v = append(*v, &models.Keyword{D: "float64"}) + *v = append(*v, keyword("float64")) return v } +// Float64 creates a token sequence containing the predeclared float64 type. func Float64() *Tokens { return create().Float64() } +// Complex64 appends the predeclared complex64 type to the token sequence. func (v *Tokens) Complex64() *Tokens { - *v = append(*v, &models.Keyword{D: "complex64"}) + *v = append(*v, keyword("complex64")) return v } +// Complex64 creates a token sequence containing the predeclared complex64 type. func Complex64() *Tokens { return create().Complex64() } +// Complex128 appends the predeclared complex128 type to the token sequence. func (v *Tokens) Complex128() *Tokens { - *v = append(*v, &models.Keyword{D: "complex128"}) + *v = append(*v, keyword("complex128")) return v } +// Complex128 creates a token sequence containing the predeclared complex128 type. func Complex128() *Tokens { return create().Complex128() } +// Byte appends the predeclared byte alias to the token sequence. func (v *Tokens) Byte() *Tokens { - *v = append(*v, &models.Keyword{D: "byte"}) + *v = append(*v, keyword("byte")) return v } +// Byte creates a token sequence containing the predeclared byte alias. func Byte() *Tokens { return create().Byte() } +// Rune appends the predeclared rune alias to the token sequence. func (v *Tokens) Rune() *Tokens { - *v = append(*v, &models.Keyword{D: "rune"}) + *v = append(*v, keyword("rune")) return v } +// Rune creates a token sequence containing the predeclared rune alias. func Rune() *Tokens { return create().Rune() } +// Uint appends the predeclared uint type to the token sequence. func (v *Tokens) Uint() *Tokens { - *v = append(*v, &models.Keyword{D: "uint"}) + *v = append(*v, keyword("uint")) return v } +// Uint creates a token sequence containing the predeclared uint type. func Uint() *Tokens { return create().Uint() } +// Int appends the predeclared int type to the token sequence. func (v *Tokens) Int() *Tokens { - *v = append(*v, &models.Keyword{D: "int"}) + *v = append(*v, keyword("int")) return v } +// Int creates a token sequence containing the predeclared int type. func Int() *Tokens { return create().Int() } +// Uintptr appends the predeclared uintptr type to the token sequence. func (v *Tokens) Uintptr() *Tokens { - *v = append(*v, &models.Keyword{D: "uintptr"}) + *v = append(*v, keyword("uintptr")) return v } +// Uintptr creates a token sequence containing the predeclared uintptr type. func Uintptr() *Tokens { return create().Uintptr() } +// String appends the predeclared string type to the token sequence. func (v *Tokens) String() *Tokens { - *v = append(*v, &models.Keyword{D: "string"}) + *v = append(*v, keyword("string")) return v } +// String creates a token sequence containing the predeclared string type. func String() *Tokens { return create().String() } +// Bool appends the predeclared bool type to the token sequence. func (v *Tokens) Bool() *Tokens { - *v = append(*v, &models.Keyword{D: "bool"}) + *v = append(*v, keyword("bool")) return v } +// Bool creates a token sequence containing the predeclared bool type. func Bool() *Tokens { return create().Bool() } +// Error appends the predeclared error interface to the token sequence. func (v *Tokens) Error() *Tokens { - *v = append(*v, &models.Keyword{D: "error"}) + *v = append(*v, keyword("error")) return v } +// Error creates a token sequence containing the predeclared error interface. func Error() *Tokens { return create().Error() } +// Map appends a map type with the supplied key and value types to the token sequence. func (v *Tokens) Map(key, val types.Token) *Tokens { return v.Join( - &models.Raw{D: "map["}, - &models.Raw{T: key}, - &models.Raw{D: "]"}, - &models.Raw{T: val}, + rawToken("map["), + rawTokenOf(key), + rawToken("]"), + rawTokenOf(val), ) } +// Map creates a token sequence containing a map type with the supplied key and value types. func Map(key, val types.Token) *Tokens { return create().Map(key, val) } diff --git a/golang/token_call.go b/golang/token_call.go index c5d63f9..11ed672 100644 --- a/golang/token_call.go +++ b/golang/token_call.go @@ -11,54 +11,64 @@ import ( ) func (v *Tokens) __defer() *Tokens { - *v = append(*v, &models.Keyword{D: "defer"}) + *v = append(*v, keyword("defer")) return v } +// Defer creates a token sequence containing the defer keyword. func Defer() *Tokens { return create().__defer() } func (v *Tokens) __go() *Tokens { - *v = append(*v, &models.Keyword{D: "go"}) + *v = append(*v, keyword("go")) return v } +// Go creates a token sequence containing the go keyword for starting a goroutine. func Go() *Tokens { return create().__go() } +// Bracket appends parentheses around the supplied comma-separated tokens to the token sequence. Use it for parameter lists and call arguments. func (v *Tokens) Bracket(args ...types.Token) *Tokens { *v = append(*v, &models.Bracket{D: args, Brace: true}) return v } +// Params creates a token sequence containing a parenthesized comma-separated parameter list. Use it when composing a function signature. func Params(args ...types.Token) *Tokens { return create().Bracket(args...) } +// Call appends a call expression with the supplied arguments to the token sequence. func (v *Tokens) Call(args ...types.Token) *Tokens { return v.Bracket(args...) } +// Call creates a token sequence containing a call expression with the supplied arguments. func Call(args ...types.Token) *Tokens { return create().Call(args...) } +// Func appends the func keyword to the token sequence. func (v *Tokens) Func() *Tokens { - *v = append(*v, &models.Keyword{D: "func"}) + *v = append(*v, keyword("func")) return v } +// Func creates a token sequence containing the func keyword. func Func() *Tokens { return create().Func() } +// Return appends the return keyword to the token sequence. func (v *Tokens) Return() *Tokens { - *v = append(*v, &models.Keyword{D: "return"}) + *v = append(*v, keyword("return")) return v } +// Return creates a token sequence containing the return keyword. func Return() *Tokens { return create().Return() } diff --git a/golang/token_comparison.go b/golang/token_comparison.go index 8023fe1..2eb50ea 100644 --- a/golang/token_comparison.go +++ b/golang/token_comparison.go @@ -5,83 +5,98 @@ package golang -import "go.osspkg.com/gogen/internal/models" - +// If appends the if keyword to the token sequence. func (v *Tokens) If() *Tokens { - *v = append(*v, &models.Keyword{D: "if"}) + *v = append(*v, keyword("if")) return v } +// If creates a token sequence containing the if keyword. func If() *Tokens { return create().If() } +// Else appends the else keyword to the token sequence. func (v *Tokens) Else() *Tokens { - *v = append(*v, &models.Keyword{D: "else"}) + *v = append(*v, keyword("else")) return v } +// ElseIf appends an else-if clause keyword to the token sequence. func (v *Tokens) ElseIf() *Tokens { - *v = append(*v, &models.Keyword{D: "else if"}) + *v = append(*v, keyword("else if")) return v } +// Default appends the default keyword for a switch clause to the token sequence. func (v *Tokens) Default() *Tokens { - *v = append(*v, &models.Keyword{D: "default"}) + *v = append(*v, keyword("default")) return v } +// Default creates a token sequence containing the default keyword for a switch clause. func Default() *Tokens { return create().Default() } +// Fallthrough appends the fallthrough keyword to the token sequence. func (v *Tokens) Fallthrough() *Tokens { - *v = append(*v, &models.Keyword{D: "fallthrough"}) + *v = append(*v, keyword("fallthrough")) return v } +// Break appends the break keyword to the token sequence. func (v *Tokens) Break() *Tokens { - *v = append(*v, &models.Keyword{D: "break"}) + *v = append(*v, keyword("break")) return v } +// Case appends the case keyword to the token sequence. func (v *Tokens) Case() *Tokens { - *v = append(*v, &models.Keyword{D: "case"}) + *v = append(*v, keyword("case")) return v } +// Case creates a token sequence containing the case keyword. func Case() *Tokens { return create().Case() } +// Continue appends the continue keyword to the token sequence. func (v *Tokens) Continue() *Tokens { - *v = append(*v, &models.Keyword{D: "continue"}) + *v = append(*v, keyword("continue")) return v } +// Goto appends the goto keyword to the token sequence. func (v *Tokens) Goto() *Tokens { - *v = append(*v, &models.Keyword{D: "goto"}) + *v = append(*v, keyword("goto")) return v } +// For appends the for keyword to the token sequence. func (v *Tokens) For() *Tokens { - *v = append(*v, &models.Keyword{D: "for"}) + *v = append(*v, keyword("for")) return v } +// For creates a token sequence containing the for keyword. func For() *Tokens { return create().For() } +// Range appends the range keyword to the token sequence. func (v *Tokens) Range() *Tokens { - *v = append(*v, &models.Keyword{D: "range"}) + *v = append(*v, keyword("range")) return v } +// Select creates a token sequence containing the select keyword. func Select() *Tokens { - return create().Join(&models.Keyword{D: "select"}) + return create().Join(keyword("select")) } +// Switch creates a token sequence containing the switch keyword. func Switch() *Tokens { - return create().Join(&models.Keyword{D: "switch"}) + return create().Join(keyword("switch")) } diff --git a/golang/token_composite.go b/golang/token_composite.go new file mode 100644 index 0000000..ec34f14 --- /dev/null +++ b/golang/token_composite.go @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package golang + +import ( + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// Index appends an index expression such as [i]. +func (v *Tokens) Index(index types.Token) *Tokens { + *v = append(*v, &models.SquareBracket{D: []types.Token{index}}) + return v +} + +// Index creates an index expression such as [i]. +func Index(index types.Token) *Tokens { + return create().Index(index) +} + +// TypeArgs appends type arguments such as [T, U]. +func (v *Tokens) TypeArgs(args ...types.Token) *Tokens { + *v = append(*v, &models.SquareBracket{D: args}) + return v +} + +// TypeArgs creates a list of type arguments such as [T, U]. +func TypeArgs(args ...types.Token) *Tokens { + return create().TypeArgs(args...) +} + +// KeyValue appends a keyed element such as key: value. +func (v *Tokens) KeyValue(key, value types.Token) *Tokens { + *v = append(*v, &models.KeyValue{Key: key, Value: value}) + return v +} + +// KeyValue creates a keyed element such as key: value. +func KeyValue(key, value types.Token) *Tokens { + return create().KeyValue(key, value) +} diff --git a/golang/token_declaration_block.go b/golang/token_declaration_block.go new file mode 100644 index 0000000..369dba8 --- /dev/null +++ b/golang/token_declaration_block.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package golang + +import ( + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// ImportBlock appends a grouped import declaration. Each spec is an import +// path, optionally preceded by an alias. +func (v *Tokens) ImportBlock(specs ...types.Token) *Tokens { + *v = append(*v, &models.DeclarationBlock{Keyword: "import", D: specs}) + return v.Line() +} + +// ImportBlock starts a token sequence with a grouped import declaration. Each +// spec is an import path, optionally preceded by an alias. +func ImportBlock(specs ...types.Token) *Tokens { + return create().ImportBlock(specs...) +} + +// TypeBlock appends a grouped type declaration. Each spec contains a type +// name followed by its underlying type. +func (v *Tokens) TypeBlock(specs ...types.Token) *Tokens { + *v = append(*v, &models.DeclarationBlock{Keyword: "type", D: specs}) + return v.Line() +} + +// TypeBlock starts a token sequence with a grouped type declaration. Each spec +// contains a type name followed by its underlying type. +func TypeBlock(specs ...types.Token) *Tokens { + return create().TypeBlock(specs...) +} diff --git a/golang/token_determinant.go b/golang/token_determinant.go index 2b198bd..9d503a6 100644 --- a/golang/token_determinant.go +++ b/golang/token_determinant.go @@ -12,107 +12,128 @@ import ( "go.osspkg.com/gogen/types" ) +// ID appends identifier text checked against the builder's accepted form to the token sequence. Render returns an error if arg is outside the builder's accepted identifier form. func (v *Tokens) ID(arg string) *Tokens { - *v = append(*v, &models.Keyword{D: arg, Verify: true}) + *v = append(*v, identifier(arg)) return v } +// ID creates a token sequence containing identifier text checked against the builder's accepted form. Render returns an error if arg is outside the builder's accepted identifier form. func ID(arg string) *Tokens { return create().ID(arg) } +// Pkg appends a package qualifier ending with a dot to the token sequence. Append the selected identifier after it. func (v *Tokens) Pkg(arg string) *Tokens { if len(arg) == 0 { return v } - *v = append(*v, &models.Raw{D: arg, Verify: true}) + *v = append(*v, verifiedRawToken(arg)) return v.Op(".") } +// Pkg creates a token sequence containing a package qualifier ending with a dot. Append the selected identifier after it. func Pkg(arg string) *Tokens { return create().Pkg(arg) } +// Type appends the type declaration keyword to the token sequence. func (v *Tokens) Type() *Tokens { - *v = append(*v, &models.Keyword{D: "type"}) + *v = append(*v, keyword("type")) return v } +// Type creates a token sequence containing the type declaration keyword. func Type() *Tokens { return create().Type() } +// Var appends the var declaration keyword to the token sequence. func (v *Tokens) Var() *Tokens { - *v = append(*v, &models.Keyword{D: "var"}) + *v = append(*v, keyword("var")) return v } +// Var creates a token sequence containing the var declaration keyword. func Var() *Tokens { return create().Var() } +// Const appends the const declaration keyword to the token sequence. func (v *Tokens) Const() *Tokens { - *v = append(*v, &models.Keyword{D: "const"}) + *v = append(*v, keyword("const")) return v } +// Const creates a token sequence containing the const declaration keyword. func Const() *Tokens { return create().Const() } +// List appends a comma-separated list without surrounding delimiters to the token sequence. Use Bracket or Call when parentheses are required. func (v *Tokens) List(args ...types.Token) *Tokens { *v = append(*v, &models.Bracket{D: args, Brace: false}) return v } +// List creates a token sequence containing a comma-separated list without surrounding delimiters. Use Bracket or Call when parentheses are required. func List(args ...types.Token) *Tokens { return create().List(args...) } +// Slice appends a slice type prefix to the token sequence. func (v *Tokens) Slice() *Tokens { - *v = append(*v, &models.Raw{D: "[]"}) + *v = append(*v, rawToken("[]")) return v } +// Slice creates a token sequence containing a slice type prefix. func Slice() *Tokens { return create().Slice() } +// Array appends an array type with the requested length to the token sequence. Render does not validate that n is a legal Go array length. func (v *Tokens) Array(n int) *Tokens { - *v = append(*v, &models.Raw{D: "[" + strconv.Itoa(n) + "]"}) + *v = append(*v, rawToken("["+strconv.Itoa(n)+"]")) return v } +// Array creates a token sequence containing an array type with the requested length. Render does not validate that n is a legal Go array length. func Array(n int) *Tokens { return create().Array(n) } +// New appends a new call for the supplied type to the token sequence. func (v *Tokens) New(arg types.Token) *Tokens { return v. Join(&models.Letter{D: "new"}). Bracket(arg) } +// New creates a token sequence containing a new call for the supplied type. func New(arg types.Token) *Tokens { return create().New(arg) } +// Make appends a make call for the requested type and sizes to the token sequence. A capacity argument is emitted only when cap is greater than len. func (v *Tokens) Make(arg types.Token, len, cap int) *Tokens { args := make([]types.Token, 0, 3) - args = append(args, arg, &models.Raw{D: strconv.Itoa(len)}) + args = append(args, arg, rawToken(strconv.Itoa(len))) if cap > len { - args = append(args, &models.Raw{D: strconv.Itoa(cap)}) + args = append(args, rawToken(strconv.Itoa(cap))) } return v.Join(&models.Letter{D: "make"}). Bracket(args...) } +// Make creates a token sequence containing a make call for the requested type and sizes. A capacity argument is emitted only when cap is greater than len. func Make(arg types.Token, len, cap int) *Tokens { return create().Make(arg, len, cap) } +// Append appends an append call with the destination and values to the token sequence. func (v *Tokens) Append(to types.Token, from ...types.Token) *Tokens { args := make([]types.Token, 0, len(from)+1) args = append(args, to) @@ -122,6 +143,7 @@ func (v *Tokens) Append(to types.Token, from ...types.Token) *Tokens { Bracket(args...) } +// Append creates a token sequence containing an append call with the destination and values. func Append(to types.Token, from ...types.Token) *Tokens { return create().Append(to, from...) } diff --git a/golang/token_field.go b/golang/token_field.go new file mode 100644 index 0000000..410b064 --- /dev/null +++ b/golang/token_field.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package golang + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type structField struct { + name string + typ types.Token + tags []string +} + +func (v *structField) Render(w io.Writer) error { + parts, err := v.tokens() + if err != nil { + return err + } + return gen.Render(w, parts) +} + +func (v *structField) RenderLayout() gen.Layout { + parts, err := v.tokens() + if err != nil { + return gen.Layout{} + } + return gen.LayoutOf(parts) +} + +func (v *structField) tokens() ([]types.Token, error) { + tag, err := (config{}).structTag(v.tags) + if err != nil { + return nil, err + } + + out := []types.Token{identifier(v.name)} + out = append(out, gen.Params(v.typ)...) + if tag != "" { + out = append(out, rawKeyword(tag)) + } + return out, nil +} + +// Field appends a named struct field with the supplied type. Tags are alternating +// keys and values; rendering returns an error if their count is odd. +func (v *Tokens) Field(name string, fieldType types.Token, tags ...string) *Tokens { + *v = append(*v, &structField{name: name, typ: fieldType, tags: tags}) + return v +} + +// Field starts a token sequence with a named struct field and optional tags. +// Tags are alternating keys and values; rendering returns an error if their count is odd. +func Field(name string, fieldType types.Token, tags ...string) *Tokens { + return create().Field(name, fieldType, tags...) +} diff --git a/golang/token_format.go b/golang/token_format.go index cfaf9de..d88a976 100644 --- a/golang/token_format.go +++ b/golang/token_format.go @@ -10,33 +10,39 @@ import ( "go.osspkg.com/gogen/types" ) +// Comment appends a Go line comment to the token sequence. func (v *Tokens) Comment(arg string) *Tokens { *v = append(*v, &models.Comment[config]{D: arg}) return v } +// Comment creates a token sequence containing a Go line comment. func Comment(arg string) *Tokens { return create().Comment(arg) } //------------------------------------------------------ +// Line appends a line break to the token sequence. func (v *Tokens) Line() *Tokens { *v = append(*v, &models.Letter{D: "\n"}) return v } +// Line creates a token sequence containing a line break. func Line() *Tokens { return create().Line() } //------------------------------------------------------ +// Block appends a brace-delimited block with indented lines to the token sequence. Nested tokens are rendered on separate indented lines. func (v *Tokens) Block(args ...types.Token) *Tokens { *v = append(*v, &models.Block{D: args}) return v } +// Block creates a token sequence containing a brace-delimited block with indented lines. Nested tokens are rendered on separate indented lines. func Block(args ...types.Token) *Tokens { return create().Block(args...) } diff --git a/golang/token_head.go b/golang/token_head.go index fd70872..3fc175d 100644 --- a/golang/token_head.go +++ b/golang/token_head.go @@ -5,33 +5,35 @@ package golang -import "go.osspkg.com/gogen/internal/models" - +// Package appends a package declaration followed by a line break to the token sequence. func (v *Tokens) Package(arg string) *Tokens { return v.Join( create().Join( - &models.Keyword{D: "package"}, - &models.Keyword{D: arg}, + keyword("package"), + keyword(arg), ), - ) + ).Line() } +// Package creates a token sequence containing a package declaration followed by a line break. func Package(arg string) *Tokens { return create().Package(arg) } //------------------------------------------------------ +// Import appends a single import declaration with an alias and path to the token sequence. name is the local import name and module is the quoted import path. func (v *Tokens) Import(name, module string) *Tokens { return v.Join( create().Join( - &models.Keyword{D: "import"}, - &models.Keyword{D: name, Verify: true}, - &models.Text{D: module}, + keyword("import"), + identifier(name), + textToken(module), ), - ) + ).Line() } +// Import creates a token sequence containing a single import declaration with an alias and path. name is the local import name and module is the quoted import path. func Import(name, module string) *Tokens { return create().Import(name, module) } diff --git a/golang/token_operation.go b/golang/token_operation.go index 8622402..097c674 100644 --- a/golang/token_operation.go +++ b/golang/token_operation.go @@ -9,29 +9,35 @@ import ( "go.osspkg.com/gogen/internal/models" ) +// Op appends a Go operator token to the token sequence. Render returns an error if the operator is not supported. func (v *Tokens) Op(arg string) *Tokens { *v = append(*v, &models.Operation[config]{D: arg}) return v } +// Op creates a token sequence containing a Go operator token. Render returns an error if the operator is not supported. func Op(arg string) *Tokens { return create().Op(arg) } +// Raw appends the supplied source text without quoting or formatting to the token sequence. Use it when the text must be inserted verbatim. func (v *Tokens) Raw(arg string) *Tokens { - *v = append(*v, &models.Keyword{D: arg}) + *v = append(*v, rawKeyword(arg)) return v } +// Raw creates a token sequence containing the supplied source text without quoting or formatting. Use it when the text must be inserted verbatim. func Raw(arg string) *Tokens { return create().Raw(arg) } +// Text appends a quoted Go string literal to the token sequence. Use it for string values that must be escaped as Go source. func (v *Tokens) Text(arg string) *Tokens { - *v = append(*v, &models.Text{D: arg}) + *v = append(*v, textToken(arg)) return v } +// Text creates a token sequence containing a quoted Go string literal. Use it for string values that must be escaped as Go source. func Text(arg string) *Tokens { return create().Text(arg) } diff --git a/golang/tokens.go b/golang/tokens.go index 2a6eb95..d160ad5 100644 --- a/golang/tokens.go +++ b/golang/tokens.go @@ -12,25 +12,24 @@ import ( "go.osspkg.com/gogen/types" ) +// Tokens is a chainable sequence of Go source tokens. type Tokens []types.Token func create() *Tokens { return &Tokens{} } +// Render writes the token sequence to w without applying go/format. func (v *Tokens) Render(w io.Writer) error { - for _, token := range *v { - if err := gen.Render(w, token); err != nil { - return err - } - } - return nil + return gen.Render(w, []types.Token(*v)) } +// Unwrap returns the underlying token slice without copying it. func (v *Tokens) Unwrap() []types.Token { return *v } +// Join appends each supplied token to the sequence and returns it. func (v *Tokens) Join(args ...types.Token) *Tokens { *v = append(*v, args...) return v diff --git a/internal/config/config.go b/internal/config/config.go index b791c65..f224e7a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,12 +5,38 @@ package config +type TokenKind uint8 + +const ( + TokenDefault TokenKind = iota + TokenDot + TokenComma + TokenColon + TokenSemicolon + TokenOpenParen + TokenCloseParen + TokenOpenSquare + TokenCloseSquare + TokenBlockOpen + TokenBlockClose + TokenTypePrefix + TokenPrefixOperator + TokenPostfixOperator +) + type Config interface { CommentSingle() OpenClose CommentMulti() OpenClose OperationAvailable(op string) bool + OperationKind(op string) TokenKind + RawKind(text string, verified bool) TokenKind + IsIdentifier(text string) bool + CanEndExpression(word string) bool + QuoteString(text string) string } type OpenClose struct { - Open, Close string + Open string + Close string + SpaceAfterOpenWhenNeeded bool } diff --git a/internal/gen/indent.go b/internal/gen/indent.go new file mode 100644 index 0000000..25a8126 --- /dev/null +++ b/internal/gen/indent.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package gen + +import ( + "bytes" + "io" +) + +type IndentWriter struct { + w io.Writer + indent int + indentText string + lineStart bool +} + +func Indented(w io.Writer) *IndentWriter { + return IndentedWith(w, "\t") +} + +// IndentedWith returns a writer that prefixes each indented line with indentText. +// If w is already an IndentWriter, it reuses that writer and its indentation. +func IndentedWith(w io.Writer, indentText string) *IndentWriter { + if indented, ok := w.(*IndentWriter); ok { + return indented + } + return &IndentWriter{w: w, indentText: indentText} +} + +func (w *IndentWriter) Push() { + w.indent++ +} + +func (w *IndentWriter) Pop() { + if w.indent > 0 { + w.indent-- + } +} + +func (w *IndentWriter) AtLineStart() bool { + return w.lineStart +} + +func (w *IndentWriter) EnsureNewline() error { + if w.lineStart { + return nil + } + _, err := w.Write([]byte{'\n'}) + return err +} + +func (w *IndentWriter) Write(p []byte) (int, error) { + consumed := 0 + for consumed < len(p) { + if w.lineStart && p[consumed] != '\n' { + for i := 0; i < w.indent; i++ { + n, err := io.WriteString(w.w, w.indentText) + if err != nil { + return consumed, err + } + if n != len(w.indentText) { + return consumed, io.ErrShortWrite + } + } + w.lineStart = false + } + + start := consumed + nextLine := bytes.IndexByte(p[start:], '\n') + end := len(p) + if nextLine >= 0 { + end = start + nextLine + 1 + } + written, err := w.w.Write(p[start:end]) + consumed += written + if written > 0 && p[consumed-1] == '\n' { + w.lineStart = true + } + if err != nil { + return consumed, err + } + if written < end-start { + return consumed, io.ErrShortWrite + } + } + return consumed, nil +} + +// WriteVerbatim writes token text without inserting indentation after its own +// newlines. It still indents the token's first line when it begins a block line. +func WriteVerbatim(w io.Writer, text string) error { + if indented, ok := w.(*IndentWriter); ok { + if indented.lineStart && text != "" && text[0] != '\n' { + for i := 0; i < indented.indent; i++ { + n, err := io.WriteString(indented.w, indented.indentText) + if err != nil { + return err + } + if n != len(indented.indentText) { + return io.ErrShortWrite + } + } + indented.lineStart = false + } + n, err := io.WriteString(indented.w, text) + if n > 0 { + indented.lineStart = text[n-1] == '\n' + } + if err != nil { + return err + } + if n != len(text) { + return io.ErrShortWrite + } + return nil + } + _, err := io.WriteString(w, text) + return err +} diff --git a/internal/gen/render.go b/internal/gen/render.go index babedd7..7f0c95e 100644 --- a/internal/gen/render.go +++ b/internal/gen/render.go @@ -21,58 +21,170 @@ func Render(w io.Writer, args ...any) error { return nil } -func drawAny(w io.Writer, t any) error { - if v, ok := t.(Unwrap); ok { - return drawList(w, v.Unwrap(), true) - } else if v, ok := t.([]types.Token); ok { - return drawList(w, v, false) - } else if v, ok := t.(types.Token); ok { - return v.Render(w) - } else if v, ok := t.(string); ok { - _, err := io.WriteString(w, v) - return err - } else { - fmt.Printf("[X] %T\n", t) +func drawAny(w io.Writer, arg any) error { + switch v := arg.(type) { + case Unwrap: + return drawList(w, v.Unwrap()) + case []types.Token: + return drawList(w, v) + case types.Token: + return drawList(w, []types.Token{v}) + case string: + n, err := io.WriteString(w, v) + if err != nil { + return err + } + if n != len(v) { + return io.ErrShortWrite + } + return nil + default: + fmt.Printf("[X] %T\n", arg) + return nil } - return nil } -func drawList(w io.Writer, list []types.Token, line bool) error { - count := len(list) - 1 - for i, token := range list { - if vt, ok := token.(Unwrap); ok { - if err := drawList(w, vt.Unwrap(), true); err != nil { - return err +func drawList(w io.Writer, list []types.Token) error { + list = flatten(list) + + var ( + previous Style + havePrev bool + ) + for _, token := range list { + layout := layoutOf(token) + first := layout.First + if first.Kind == KindUnaryOperator { + if !havePrev || !canEndExpression(previous) { + first.Kind = KindPrefixOperator + layout.First.Kind = KindPrefixOperator + if layout.Last.Kind == KindUnaryOperator { + layout.Last.Kind = KindPrefixOperator + } + } else { + first.Kind = KindOperator + layout.First.Kind = KindOperator + if layout.Last.Kind == KindUnaryOperator { + layout.Last.Kind = KindOperator + } } - continue } + if havePrev { + if _, err := io.WriteString(w, separator(previous, first)); err != nil { + return err + } + } if err := token.Render(w); err != nil { return err } - if i >= count { - continue - } - if _, ok := token.(NoSpace); ok { + previous = layout.Last + havePrev = previous.Kind != KindLine + } + return nil +} + +func flatten(in []types.Token) []types.Token { + out := make([]types.Token, 0, len(in)) + for _, token := range in { + if nested, ok := token.(Unwrap); ok { + out = append(out, flatten(nested.Unwrap())...) continue } - if _, err := io.WriteString(w, " "); err != nil { - return err - } + out = append(out, token) } - if line { - if _, err := io.WriteString(w, "\n"); err != nil { - return err - } + return out +} + +func layoutOf(token types.Token) Layout { + if styled, ok := token.(Styled); ok { + return styled.RenderLayout() } - return nil + unknown := Style{Kind: KindUnknown} + return Layout{First: unknown, Last: unknown} } -func Params(token types.Token) (out []types.Token) { - if unw, ok := token.(Unwrap); ok { - out = append(out, unw.Unwrap()...) - } else { - out = append(out, token) +func separator(previous, next Style) string { + if noSeparator(previous, next) { + return "" + } + if previous.Kind == KindBlockOpen { + return "" + } + if previous.Kind == KindComma || previous.Kind == KindColon || previous.Kind == KindOperator { + return " " + } + if previous.Kind == KindPrefixOperator || previous.Kind == KindPostfixOperator { + return "" + } + if next.Kind == KindOperator { + return " " + } + if next.Kind == KindPrefixOperator { + return wordLike(previous) + } + if next.Kind == KindPostfixOperator { + return "" + } + return " " +} + +func noSeparator(previous, next Style) bool { + return previous.Kind == KindLine || + next.Kind == KindLine || + previous.Kind == KindComment || + next.Kind == KindComma || + next.Kind == KindDot || + next.Kind == KindColon || + next.Kind == KindSemicolon || + next.Kind == KindCloseParen || + next.Kind == KindCloseBracket || + previous.Kind == KindDot || + previous.Kind == KindOpenParen || + previous.Kind == KindOpenSquare || + previous.Kind == KindTypePrefix || + previous.Kind == KindCloseBracket || + next.Kind == KindOpenParen || + next.Kind == KindOpenSquare +} + +func wordLike(style Style) string { + if wordLikeKind(style.Kind) { + return " " + } + return "" +} + +func wordLikeKind(kind Kind) bool { + return kind == KindWord || kind == KindLiteral || kind == KindUnknown +} + +func canEndExpression(style Style) bool { + switch style.Kind { + case KindWord: + return style.CanEndExpression + case KindLiteral, KindFragment, KindTypePrefix, KindCloseBracket, KindCloseParen, KindBlockClose, KindPostfixOperator: + return true + default: + return false + } +} + +// LayoutOf returns the outer token styles of a token sequence after unwrapping +// nested token builders. +func LayoutOf(tokens []types.Token) Layout { + flat := flatten(tokens) + if len(flat) == 0 { + return Layout{} + } + first := layoutOf(flat[0]) + last := layoutOf(flat[len(flat)-1]) + return Layout{First: first.First, Last: last.Last} +} + +func Params(token types.Token) []types.Token { + if nested, ok := token.(Unwrap); ok { + return nested.Unwrap() } - return + return []types.Token{token} } diff --git a/internal/gen/symbol.go b/internal/gen/symbol.go new file mode 100644 index 0000000..0fee34f --- /dev/null +++ b/internal/gen/symbol.go @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package gen + +import ( + "io" + + "go.osspkg.com/gogen/types" +) + +type symbol struct { + text string + style Style +} + +func Symbol(text string, style Style) types.Token { + return symbol{text: text, style: style} +} + +func (v symbol) Render(w io.Writer) error { + _, err := io.WriteString(w, v.text) + return err +} + +func (v symbol) RenderLayout() Layout { + return Layout{First: v.style, Last: v.style} +} diff --git a/internal/gen/types.go b/internal/gen/types.go index a4b4c2f..a109fb1 100644 --- a/internal/gen/types.go +++ b/internal/gen/types.go @@ -11,6 +11,43 @@ type Unwrap interface { Unwrap() []types.Token } -type NoSpace interface { - NoSpace() +type Kind uint8 + +const ( + KindUnknown Kind = iota + KindWord + KindLiteral + KindFragment + KindTypePrefix + KindCloseBracket + KindOperator + KindUnaryOperator + KindPrefixOperator + KindPostfixOperator + KindDot + KindComma + KindColon + KindSemicolon + KindOpenParen + KindCloseParen + KindOpenSquare + KindBlockOpen + KindBlockClose + KindLine + KindComment +) + +type Style struct { + Kind Kind + Text string + CanEndExpression bool +} + +type Layout struct { + First Style + Last Style +} + +type Styled interface { + RenderLayout() Layout } diff --git a/internal/models/block.go b/internal/models/block.go index 190fecb..7ee60df 100644 --- a/internal/models/block.go +++ b/internal/models/block.go @@ -17,16 +17,41 @@ type Block struct { } func (v *Block) Render(w io.Writer) error { - out := make([]any, 0) - out = append(out, "{") - if len(v.D) > 0 { - out = append(out, "\n") + iw := gen.Indented(w) + if len(v.D) == 0 { + return gen.Render(iw, []types.Token{ + gen.Symbol("{", gen.Style{Kind: gen.KindBlockOpen}), + gen.Symbol("}", gen.Style{Kind: gen.KindBlockClose}), + }) } + + if err := gen.Render(iw, gen.Symbol("{", gen.Style{Kind: gen.KindBlockOpen})); err != nil { + return err + } + iw.Push() for _, token := range v.D { - out = append(out, token) + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + if err := gen.Render(iw, token); err != nil { + iw.Pop() + return err + } + } + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + iw.Pop() + return gen.Render(iw, gen.Symbol("}", gen.Style{Kind: gen.KindBlockClose})) +} + +func (v *Block) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindBlockOpen}, + Last: gen.Style{Kind: gen.KindBlockClose}, } - out = append(out, "}") - return gen.Render(w, out...) } type Bracket struct { @@ -35,19 +60,28 @@ type Bracket struct { } func (v *Bracket) Render(w io.Writer) error { - count := len(v.D) - 1 - out := make([]any, 0, count*2+2) + out := make([]types.Token, 0, len(v.D)*2+2) if v.Brace { - out = append(out, "(") + out = append(out, gen.Symbol("(", gen.Style{Kind: gen.KindOpenParen})) } for i, token := range v.D { - out = append(out, gen.Params(token)) - if i < count { - out = append(out, ", ") + if i > 0 { + out = append(out, gen.Symbol(",", gen.Style{Kind: gen.KindComma})) } + out = append(out, gen.Params(token)...) + } + if v.Brace { + out = append(out, gen.Symbol(")", gen.Style{Kind: gen.KindCloseParen})) } + return gen.Render(w, out) +} + +func (v *Bracket) RenderLayout() gen.Layout { if v.Brace { - out = append(out, ")") + return gen.Layout{ + First: gen.Style{Kind: gen.KindOpenParen}, + Last: gen.Style{Kind: gen.KindCloseParen}, + } } - return gen.Render(w, out...) + return gen.Layout{} } diff --git a/internal/models/comment.go b/internal/models/comment.go index e03ba51..2f6a3cf 100644 --- a/internal/models/comment.go +++ b/internal/models/comment.go @@ -24,5 +24,13 @@ func (v *Comment[C]) Render(w io.Writer) error { oc = v.c.CommentMulti() } + if oc.SpaceAfterOpenWhenNeeded && len(v.D) > 0 && v.D[0] != ' ' && v.D[0] != '\t' { + return gen.Render(w, oc.Open, " ", v.D, oc.Close) + } return gen.Render(w, oc.Open, v.D, oc.Close) } + +func (v *Comment[C]) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindComment, Text: v.D} + return gen.Layout{First: style, Last: style} +} diff --git a/internal/models/config_test.go b/internal/models/config_test.go new file mode 100644 index 0000000..cf82c8b --- /dev/null +++ b/internal/models/config_test.go @@ -0,0 +1,69 @@ +package models + +import ( + "bytes" + "io" + "strings" + "testing" + + "go.osspkg.com/gogen/internal/config" + "go.osspkg.com/gogen/internal/gen" +) + +type alternateConfig struct{} + +func (alternateConfig) CommentSingle() config.OpenClose { + return config.OpenClose{Open: "#", Close: "\n", SpaceAfterOpenWhenNeeded: true} +} + +func (alternateConfig) CommentMulti() config.OpenClose { + return config.OpenClose{Open: "/*", Close: "*/"} +} + +func (alternateConfig) OperationAvailable(string) bool { return true } +func (alternateConfig) OperationKind(string) config.TokenKind { return config.TokenDefault } +func (alternateConfig) RawKind(string, bool) config.TokenKind { return config.TokenDefault } +func (alternateConfig) IsIdentifier(string) bool { return true } +func (alternateConfig) CanEndExpression(word string) bool { return word != "return" } +func (alternateConfig) QuoteString(value string) string { + return "'" + strings.ReplaceAll(value, "'", "\\'") + "'" +} + +func TestLanguageConfigControlsTextAndComments(t *testing.T) { + var got bytes.Buffer + if err := (&Text[alternateConfig]{D: "hello", C: alternateConfig{}}).Render(&got); err != nil { + t.Fatal(err) + } + if want := "'hello'"; got.String() != want { + t.Fatalf("Text.Render() = %q, want %q", got.String(), want) + } + + got.Reset() + if err := (&Comment[alternateConfig]{D: "note", c: alternateConfig{}}).Render(&got); err != nil { + t.Fatal(err) + } + if want := "# note\n"; got.String() != want { + t.Fatalf("Comment.Render() = %q, want %q", got.String(), want) + } +} + +func TestLanguageConfigControlsIdentifierAndOperatorLayout(t *testing.T) { + identifier := &Keyword[alternateConfig]{D: "yield", Verify: true, C: alternateConfig{}} + var got bytes.Buffer + if err := gen.Render(&got, identifier); err != nil { + t.Fatal(err) + } + if want := "yield"; got.String() != want { + t.Fatalf("identifier render = %q, want %q", got.String(), want) + } + + operator := &Operation[alternateConfig]{D: "@", c: alternateConfig{}} + if got := operator.RenderLayout().First.Kind; got != gen.KindOperator { + t.Fatalf("operator layout kind = %v, want %v", got, gen.KindOperator) + } + + var writer io.Writer = io.Discard + if err := operator.Render(writer); err != nil { + t.Fatal(err) + } +} diff --git a/internal/models/declaration_block.go b/internal/models/declaration_block.go new file mode 100644 index 0000000..19d142a --- /dev/null +++ b/internal/models/declaration_block.go @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package models + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type DeclarationBlock struct { + Keyword string + D []types.Token +} + +func (v *DeclarationBlock) Render(w io.Writer) error { + iw := gen.Indented(w) + if err := gen.Render(iw, v.Keyword+" ("); err != nil { + return err + } + if len(v.D) == 0 { + return gen.Render(iw, gen.Symbol(")", gen.Style{Kind: gen.KindCloseParen})) + } + + iw.Push() + for _, token := range v.D { + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + if err := gen.Render(iw, token); err != nil { + iw.Pop() + return err + } + } + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + iw.Pop() + return gen.Render(iw, gen.Symbol(")", gen.Style{Kind: gen.KindCloseParen})) +} + +func (v *DeclarationBlock) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindWord, Text: v.Keyword}, + Last: gen.Style{Kind: gen.KindCloseParen, Text: ")"}, + } +} diff --git a/internal/models/key_value.go b/internal/models/key_value.go new file mode 100644 index 0000000..099044a --- /dev/null +++ b/internal/models/key_value.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package models + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type KeyValue struct { + Key types.Token + Value types.Token +} + +func (v *KeyValue) Render(w io.Writer) error { + return gen.Render(w, []types.Token{ + v.Key, + gen.Symbol(":", gen.Style{Kind: gen.KindColon}), + v.Value, + }) +} + +func (v *KeyValue) RenderLayout() gen.Layout { + return gen.LayoutOf([]types.Token{ + v.Key, + gen.Symbol(":", gen.Style{Kind: gen.KindColon}), + v.Value, + }) +} diff --git a/internal/models/keyword.go b/internal/models/keyword.go index 4b21c86..5fccf7b 100644 --- a/internal/models/keyword.go +++ b/internal/models/keyword.go @@ -8,21 +8,37 @@ package models import ( "fmt" "io" - "regexp" + "go.osspkg.com/gogen/internal/config" "go.osspkg.com/gogen/internal/gen" ) -type Keyword struct { +type Keyword[C config.Config] struct { + C C Verify bool + Raw bool D string } -var rexLetter = regexp.MustCompile(`(?mUi)^[a-z][0-9a-z\_]{0,}$`) - -func (v *Keyword) Render(w io.Writer) error { - if v.Verify && !rexLetter.MatchString(v.D) { - return fmt.Errorf("invalid letter: %s", v.D) +func (v *Keyword[C]) Render(w io.Writer) error { + if v.Verify && !v.C.IsIdentifier(v.D) { + return fmt.Errorf("invalid identifier: %s", v.D) + } + if v.Raw { + return gen.WriteVerbatim(w, v.D) } return gen.Render(w, v.D) } + +func (v *Keyword[C]) RenderLayout() gen.Layout { + kind := gen.KindWord + if v.Raw { + kind = gen.KindFragment + } + style := gen.Style{ + Kind: kind, + Text: v.D, + CanEndExpression: v.C.CanEndExpression(v.D), + } + return gen.Layout{First: style, Last: style} +} diff --git a/internal/models/letter.go b/internal/models/letter.go index 7cd9446..b87d1ce 100644 --- a/internal/models/letter.go +++ b/internal/models/letter.go @@ -9,6 +9,7 @@ import ( "fmt" "io" + "go.osspkg.com/gogen/internal/config" "go.osspkg.com/gogen/internal/gen" "go.osspkg.com/gogen/types" ) @@ -21,16 +22,26 @@ func (v *Letter) Render(w io.Writer) error { return gen.Render(w, v.D) } -type Raw struct { +func (v *Letter) RenderLayout() gen.Layout { + kind := gen.KindWord + if v.D == "\n" { + kind = gen.KindLine + } + style := gen.Style{Kind: kind, Text: v.D} + return gen.Layout{First: style, Last: style} +} + +type Raw[C config.Config] struct { + C C D string T types.Token AT []types.Token Verify bool } -func (v *Raw) Render(w io.Writer) error { - if v.Verify && !rexLetter.MatchString(v.D) { - return fmt.Errorf("invalid letter: %s", v.D) +func (v *Raw[C]) Render(w io.Writer) error { + if v.Verify && !v.C.IsIdentifier(v.D) { + return fmt.Errorf("invalid identifier: %s", v.D) } if err := gen.Render(w, v.D); err != nil { return err @@ -50,4 +61,18 @@ func (v *Raw) Render(w io.Writer) error { return nil } -func (v *Raw) NoSpace() {} +func (v *Raw[C]) RenderLayout() gen.Layout { + if v.D == "" && v.T != nil { + return gen.LayoutOf([]types.Token{v.T}) + } + kind := layoutKind(v.C.RawKind(v.D, v.Verify)) + if v.Verify { + kind = gen.KindWord + } + style := gen.Style{ + Kind: kind, + Text: v.D, + CanEndExpression: v.C.CanEndExpression(v.D), + } + return gen.Layout{First: style, Last: style} +} diff --git a/internal/models/operation.go b/internal/models/operation.go index 8f652b3..3ae5477 100644 --- a/internal/models/operation.go +++ b/internal/models/operation.go @@ -25,4 +25,7 @@ func (v *Operation[C]) Render(w io.Writer) error { return gen.Render(w, v.D) } -func (v *Operation[C]) NoSpace() {} +func (v *Operation[C]) RenderLayout() gen.Layout { + style := gen.Style{Kind: layoutKind(v.c.OperationKind(v.D)), Text: v.D} + return gen.Layout{First: style, Last: style} +} diff --git a/internal/models/square_bracket.go b/internal/models/square_bracket.go new file mode 100644 index 0000000..b77ab68 --- /dev/null +++ b/internal/models/square_bracket.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package models + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type SquareBracket struct { + D []types.Token +} + +func (v *SquareBracket) Render(w io.Writer) error { + out := make([]types.Token, 0, len(v.D)*2+2) + out = append(out, gen.Symbol("[", gen.Style{Kind: gen.KindOpenSquare})) + for i, token := range v.D { + if i > 0 { + out = append(out, gen.Symbol(",", gen.Style{Kind: gen.KindComma})) + } + out = append(out, gen.Params(token)...) + } + out = append(out, gen.Symbol("]", gen.Style{Kind: gen.KindCloseBracket})) + return gen.Render(w, out) +} + +func (v *SquareBracket) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindOpenSquare}, + Last: gen.Style{Kind: gen.KindCloseBracket}, + } +} diff --git a/internal/models/style.go b/internal/models/style.go new file mode 100644 index 0000000..7a6cdad --- /dev/null +++ b/internal/models/style.go @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package models + +import ( + "go.osspkg.com/gogen/internal/config" + "go.osspkg.com/gogen/internal/gen" +) + +func layoutKind(kind config.TokenKind) gen.Kind { + switch kind { + case config.TokenDot: + return gen.KindDot + case config.TokenComma: + return gen.KindComma + case config.TokenColon: + return gen.KindColon + case config.TokenSemicolon: + return gen.KindSemicolon + case config.TokenOpenParen: + return gen.KindOpenParen + case config.TokenCloseParen: + return gen.KindCloseParen + case config.TokenOpenSquare: + return gen.KindOpenSquare + case config.TokenCloseSquare: + return gen.KindCloseBracket + case config.TokenBlockOpen: + return gen.KindBlockOpen + case config.TokenBlockClose: + return gen.KindBlockClose + case config.TokenTypePrefix: + return gen.KindTypePrefix + case config.TokenPrefixOperator: + return gen.KindUnaryOperator + case config.TokenPostfixOperator: + return gen.KindPostfixOperator + default: + return gen.KindOperator + } +} + +// LayoutKind converts a language-neutral token category into its renderer style. +func LayoutKind(kind config.TokenKind) gen.Kind { + return layoutKind(kind) +} diff --git a/internal/models/text.go b/internal/models/text.go index 7bc1070..9a187da 100644 --- a/internal/models/text.go +++ b/internal/models/text.go @@ -7,15 +7,21 @@ package models import ( "io" - "strconv" + "go.osspkg.com/gogen/internal/config" "go.osspkg.com/gogen/internal/gen" ) -type Text struct { +type Text[C config.Config] struct { + C C D string } -func (v *Text) Render(w io.Writer) error { - return gen.Render(w, strconv.Quote(v.D)) +func (v *Text[C]) Render(w io.Writer) error { + return gen.Render(w, v.C.QuoteString(v.D)) +} + +func (v *Text[C]) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindLiteral, Text: v.D} + return gen.Layout{First: style, Last: style} } diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..99a16e5 --- /dev/null +++ b/python/README.md @@ -0,0 +1,118 @@ +# Python code builder + +The `go.osspkg.com/gogen/python` package builds Python 3.10+ source from composable tokens. Package-level constructors create expressions and statement headers; fluent `*Tokens` methods append more tokens. `Tokens.Render` and package-level `python.Render` write readable source directly without formatting or interpreter validation. + +```sh +go get go.osspkg.com/gogen/python +``` + +## Build a module + +```go +package main + +import ( + "bytes" + "fmt" + + py "go.osspkg.com/gogen/python" +) + +func main() { + file := py.FromImport("dataclasses", py.ID("dataclass")).Line().Join( + py.Decorator(py.ID("dataclass")), + py.Class("User").Block( + py.ID("name").Colon().String(), + py.ID("active").Colon().Bool().Op("=").True(), + ), + ).Line().Join( + py.AsyncDef("load_user").Bracket(py.ID("user_id").Colon().Int()).Arrow().TypeUnion(py.ID("User"), py.NoneValue()).Block( + py.If(py.ID("user_id").Op("<").Raw("0")).Block(py.Return().NoneValue()), + py.Return().Await().ID("repository").Selector("load").Call(py.ID("user_id")), + ), + ) + + var source bytes.Buffer + if err := py.Render(&source, file); err != nil { + panic(err) + } + fmt.Print(source.String()) +} +``` + +The rendered source uses four spaces for each suite level: + +```python +from dataclasses import dataclass +@dataclass +class User: + name: str + active: bool = True + +async def load_user(user_id: int) -> User | None: + if user_id < 0: + return None + return await repository.load(user_id) +``` + +`Line()` explicitly separates top-level statements. Nested suite builders add their own line breaks and indentation. Empty suites render as `pass`. + +## Declarations and control flow + +Use `Import`, `ImportAs`, and `FromImport` for imports. `Def`, `AsyncDef`, and `Class` return headers that can be extended with `Bracket`, `Colon`, `Arrow`, and `Block`. `Decorator` emits a decorator on its own line. Control headers include `If`, `Elif`, `Else`, `ForEach`, `AsyncForEach`, `While`, `With`, `AsyncWith`, `Try`, `Except`, `Finally`, `Match`, and `Case`. Call `Block` on a header to add its suite. Clause builders stay aligned after the preceding suite: + +```go +py.If(py.ID("ready")).Block(py.Return().True()).Else().Block(py.Return().False()) +``` + +## Expressions and values + +| Builder | Use | +| --- | --- | +| `ID`, `Pkg`, `Selector` | Validate identifiers or compose dotted access. | +| `Raw`, `Text`, `FString`, `FStringText`, `FStringExpr` | Add caller-provided source, escaped strings, and interpolated strings. | +| `Op`, `Call`, `Bracket`, `Index`, `List`, `KeyValue` | Compose operators, calls, grouped arguments, indexing, and key/value entries. | +| `ListLiteral`, `TupleLiteral`, `SetLiteral`, `DictLiteral` | Build common collection literals. | +| `ListComp`, `SetComp`, `DictComp`, `GeneratorExpr` | Build comprehensions with `ForClause`, `AsyncForClause`, and `IfClause`. | +| `TypeUnion` | Compose Python 3.10 union annotations with `|`. | +| `Int`, `Float`, `Complex`, `Bool`, `String`, `Str`, `Bytes`, `ByteArray`, `MemoryView`, `Range`, `ObjectType`, `TypeType`, `Any` | Emit built-in types; `Any` emits the name from `typing`. | +| `ListType`, `DictType`, `SetType`, `FrozenSetType`, `TupleType` | Emit PEP 585 generic container types. | +| `NoneValue`, `True`, `False`, `Ellipsis` | Emit Python's built-in literal values. | + +`Text` and f-string text parts escape their respective string contexts. F-string expressions accept tokens; advanced conversion flags and format specifications can be composed with `Raw`. + +## Built-in types + +Use the type builders in annotations and generic expressions. They emit Python's built-in type names and PEP 585 forms such as `list[str]`, `dict[str, int]`, and `tuple[str, ...]`: + +```go +py.Def("load_names").Bracket().Arrow().ListType(py.String()).Block( + py.Return().ListLiteral(py.Text("Ada")), +) +``` + +`Int`, `Float`, `Complex`, `Bool`, `String`/`Str`, `Bytes`, `ByteArray`, `MemoryView`, `Range`, `ObjectType`, and `TypeType` emit built-in type names. Use `ListType`, `DictType`, `SetType`, `FrozenSetType`, and `TupleType` for generic containers. `TupleType()` without arguments emits bare `tuple`; `TupleType(py.Int(), py.Ellipsis())` emits `tuple[int, ...]`. `Any()` emits `Any` and expects the generated module to import it from `typing`. For `None`, use `NoneValue()`; unions can be composed with `TypeUnion`. + +## Builder reference + +Package-level constructors and chainable `*Tokens` methods are available unless the table identifies a component token. Component constructors return `types.Token`; use them as arguments to the parent builder. + +| Area | Builders | Notes | +| --- | --- | --- | +| Imports and declarations | `Import`, `ImportAs`, `FromImport`, `Decorator`, `Def`, `AsyncDef`, `Class` | Build imports, decorators, function headers, and class headers. | +| Control flow | `If`, `Elif`, `Else`, `ForEach`, `AsyncForEach`, `While`, `With`, `AsyncWith`, `Try`, `Except`, `Finally`, `Match`, `Case` | Append `Block` to a header to emit its suite. | +| Statements and syntax | `Return`, `Yield`, `Raise`, `Pass`, `Break`, `Continue`, `Assert`, `Del`, `Global`, `Nonlocal`, `Await`, `As`, `Arrow`, `Colon`, `Comma` | Compose statements, async expressions, annotations, aliases, and punctuation. | +| Identifiers and expressions | `ID`, `Pkg`, `Selector`, `Raw`, `Text`, `Op`, `Call`, `Bracket`, `Index`, `List`, `KeyValue`, `TypeUnion` | `Raw` writes source verbatim; `Text` escapes a Python string literal. `List` is a comma-separated sequence without delimiters. | +| Built-in types | `Any`, `Int`, `Float`, `Complex`, `Bool`, `String`, `Str`, `Bytes`, `ByteArray`, `MemoryView`, `Range`, `ObjectType`, `TypeType` | `Any` emits `typing.Any`; generated modules must import it. `String` and `Str` both emit `str`. | +| Generic types | `ListType`, `DictType`, `SetType`, `FrozenSetType`, `TupleType` | Emit Python 3.10-compatible PEP 585 annotations. | +| Collection values | `ListLiteral`, `TupleLiteral`, `SetLiteral`, `DictLiteral` | Build list, tuple, set, and dictionary expressions. | +| Comprehensions | `ListComp`, `SetComp`, `DictComp`, `GeneratorExpr` | Combine with the component constructors `ForClause`, `AsyncForClause`, and `IfClause`. | +| F-strings and values | `FString`, `NoneValue`, `True`, `False`, `Ellipsis` | `FStringText` and `FStringExpr` create f-string parts; they return `types.Token`. | +| Layout and output | `Block`, `Comment`, `Line`, `Join`, `Render`, `Unwrap` | `Block` emits a colon and an indented suite; `Line` adds an explicit top-level line break. | + + +## Errors and custom tokens + +`ID` validates Unicode identifier syntax and rejects Python keywords. `Op` rejects operators outside the adapter's supported set. Rendering propagates token and writer errors, but does not check Python grammar, names, types, or runtime behavior. `Raw` is written verbatim; use it for syntax controlled by the caller. To provide a custom builder, implement [`types.Token`](../types/token.go) and write its source in `Render(io.Writer) error`. + +For other language adapters and repository information, see the [root README](../README.md), [Go guide](../golang/README.md), and [TypeScript/TSX guide](../typescript/README.md). The package reference is on [pkg.go.dev](https://pkg.go.dev/go.osspkg.com/gogen/python). diff --git a/python/config.go b/python/config.go new file mode 100644 index 0000000..7f04923 --- /dev/null +++ b/python/config.go @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "fmt" + "strings" + "unicode" + + "go.osspkg.com/gogen/internal/config" +) + +var _ config.Config = languageConfig{} + +type languageConfig struct{} + +var reservedWords = map[string]struct{}{ + "False": {}, "None": {}, "True": {}, "and": {}, "as": {}, "assert": {}, "async": {}, + "await": {}, "break": {}, "class": {}, "continue": {}, "def": {}, "del": {}, "elif": {}, + "else": {}, "except": {}, "finally": {}, "for": {}, "from": {}, "global": {}, "if": {}, + "import": {}, "in": {}, "is": {}, "lambda": {}, "nonlocal": {}, "not": {}, "or": {}, + "pass": {}, "raise": {}, "return": {}, "try": {}, "while": {}, "with": {}, "yield": {}, +} + +func (languageConfig) OperationAvailable(op string) bool { + switch op { + case "+", "-", "*", "**", "/", "//", "%", "@", "<<", ">>", "&", "|", "^", "~", + "+=", "-=", "*=", "**=", "/=", "//=", "%=", "@=", "<<=", ">>=", "&=", "|=", "^=", + "==", "!=", "<", ">", "<=", ">=", "=", ":=", "and", "or", "not", "in", "is", "is not", "not in", + "->", ".", ",", ":", "(", ")", "[", "]", "{", "}": + return true + default: + return false + } +} + +func (languageConfig) OperationKind(op string) config.TokenKind { + switch op { + case ".": + return config.TokenDot + case ",": + return config.TokenComma + case ":": + return config.TokenColon + case "(": + return config.TokenOpenParen + case ")": + return config.TokenCloseParen + case "[": + return config.TokenOpenSquare + case "]": + return config.TokenCloseSquare + case "{": + return config.TokenBlockOpen + case "}": + return config.TokenBlockClose + case "+", "-", "*", "**", "~": + return config.TokenPrefixOperator + default: + return config.TokenDefault + } +} + +func (languageConfig) RawKind(text string, verified bool) config.TokenKind { + if verified { + return config.TokenDefault + } + switch text { + case "]": + return config.TokenCloseSquare + case "}": + return config.TokenBlockClose + case ".": + return config.TokenDot + default: + return config.TokenDefault + } +} + +func (languageConfig) IsIdentifier(text string) bool { + if text == "" { + return false + } + if _, reserved := reservedWords[text]; reserved { + return false + } + for index, r := range text { + if index == 0 { + if !isIdentifierStart(r) { + return false + } + continue + } + if !isIdentifierContinue(r) { + return false + } + } + return true +} + +func isIdentifierStart(r rune) bool { + return r == '_' || unicode.IsLetter(r) || unicode.In(r, unicode.Nl) +} + +func isIdentifierContinue(r rune) bool { + return isIdentifierStart(r) || unicode.IsDigit(r) || unicode.In(r, unicode.Mn, unicode.Mc, unicode.Pc) || r == '\u200c' || r == '\u200d' +} + +func (languageConfig) CanEndExpression(word string) bool { + switch strings.TrimSpace(word) { + case "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield": + return false + default: + return true + } +} + +func (languageConfig) QuoteString(value string) string { + var out strings.Builder + out.Grow(len(value) + 2) + out.WriteByte('"') + for _, r := range value { + switch r { + case '\\', '"': + out.WriteByte('\\') + out.WriteRune(r) + case '\n': + out.WriteString("\\n") + case '\r': + out.WriteString("\\r") + case '\t': + out.WriteString("\\t") + case '\b': + out.WriteString("\\x08") + case '\f': + out.WriteString("\\x0c") + default: + if r < 0x20 || r == 0x7f { + out.WriteString(fmt.Sprintf("\\x%02x", r)) + } else { + out.WriteRune(r) + } + } + } + out.WriteByte('"') + return out.String() +} + +func (languageConfig) CommentSingle() config.OpenClose { + return config.OpenClose{Open: "#", Close: "\n", SpaceAfterOpenWhenNeeded: true} +} + +func (languageConfig) CommentMulti() config.OpenClose { + return config.OpenClose{Open: "#", Close: "\n", SpaceAfterOpenWhenNeeded: true} +} diff --git a/python/doc.go b/python/doc.go new file mode 100644 index 0000000..7430181 --- /dev/null +++ b/python/doc.go @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package python builds Python 3.10+ source from composable tokens. It includes +// builders for declarations, control flow, expressions, values, and built-in or +// generic type annotations. Suites use four-space indentation; callers insert +// explicit Line tokens between top-level statements. Rendering writes readable +// source directly and does not format, parse, or type-check the generated code. +package python diff --git a/python/example_test.go b/python/example_test.go new file mode 100644 index 0000000..8721dc5 --- /dev/null +++ b/python/example_test.go @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python_test + +import ( + "bytes" + "fmt" + + py "go.osspkg.com/gogen/python" +) + +func ExampleRender() { + file := py.FromImport("dataclasses", py.ID("dataclass")).Line().Join( + py.Decorator(py.ID("dataclass")), + py.Class("User").Block( + py.ID("name").Colon().ID("str"), + py.ID("active").Colon().ID("bool").Op("=").True(), + ), + py.Def("greet").Bracket(py.ID("user").Colon().ID("User")).Arrow().ID("str").Block( + py.Return().FString(py.FStringText("Hello, "), py.FStringExpr(py.ID("user").Selector("name"))), + ), + ) + + var source bytes.Buffer + if err := py.Render(&source, file); err != nil { + panic(err) + } + fmt.Print(source.String()) + // Output: + // from dataclasses import dataclass + // @dataclass + // class User: + // name: str + // active: bool = True + // def greet(user: User) -> str: + // return f"Hello, {user.name}" +} diff --git a/python/render_test.go b/python/render_test.go new file mode 100644 index 0000000..5b66a59 --- /dev/null +++ b/python/render_test.go @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python_test + +import ( + "bytes" + "errors" + "io" + "testing" + + py "go.osspkg.com/gogen/python" + "go.osspkg.com/gogen/types" +) + +func render(t *testing.T, token interface{ Render(io.Writer) error }) string { + t.Helper() + var output bytes.Buffer + if err := token.Render(&output); err != nil { + t.Fatal(err) + } + return output.String() +} + +func TestRenderDeclarationsAndSuites(t *testing.T) { + file := py.FromImport("typing", py.ID("Sequence").As().ID("Seq")).Line().Join( + py.Decorator(py.ID("trace")), + py.Def("greet").Bracket(py.ID("name").Colon().ID("str")).Arrow().TypeUnion(py.ID("str"), py.NoneValue()).Block( + py.If(py.ID("name").Op("==").Text("")).Block( + py.Return().Text("anonymous"), + ).Elif(py.ID("name").Op("==").Text("admin")).Block( + py.Return().Text("welcome"), + ).Else().Block( + py.Return().Op("not").ID("name"), + ), + ), + ) + want := "from typing import Sequence as Seq\n@trace\ndef greet(name: str) -> str | None:\n if name == \"\":\n return \"anonymous\"\n elif name == \"admin\":\n return \"welcome\"\n else:\n return not name\n" + if got := render(t, file); got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestRenderImportsClassesAndAsync(t *testing.T) { + file := py.ImportAs("collections.abc", "abc").Line().Join( + py.Class("Repository", py.ID("abc").Selector("Sequence")).Block( + py.AsyncDef("load").Bracket(py.ID("self"), py.ID("key").Colon().ID("str")).Arrow().ID("bytes").Block( + py.With(py.ID("open").Call(py.ID("key"), py.Text("rb")).As().ID("stream")).Block( + py.Return().Await().ID("stream").Selector("read").Call(), + ), + ), + ), + ) + want := "import collections.abc as abc\nclass Repository(abc.Sequence):\n async def load(self, key: str) -> bytes:\n with open(key, \"rb\") as stream:\n return await stream.read()\n" + if got := render(t, file); got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestRenderTryMatchAndEmptySuite(t *testing.T) { + token := py.Try().Block( + py.Pass(), + ).Except(py.ID("ValueError")).Block( + py.Raise(), + ).Finally().Block( + py.Pass(), + ).Join( + py.Match(py.ID("value")).Block( + py.Case(py.Raw("0")).Block(py.Pass()), + py.Case(py.Raw("_")).Block(py.Pass()), + ), + py.Def("empty").Bracket().Block(), + ) + want := "try:\n pass\nexcept ValueError:\n raise\nfinally:\n pass\nmatch value:\n case 0:\n pass\n case _:\n pass\ndef empty(): pass\n" + if got := render(t, token); got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestRenderContainersComprehensionsAndFString(t *testing.T) { + tests := []struct { + name string + token types.Token + want string + }{ + {name: "list", token: py.ListLiteral(py.Raw("1"), py.Raw("2")), want: "[1, 2]"}, + {name: "empty set", token: py.SetLiteral(), want: "set()"}, + {name: "set", token: py.SetLiteral(py.Raw("1"), py.Raw("2")), want: "{1, 2}"}, + {name: "dict", token: py.DictLiteral(py.KeyValue(py.Text("x"), py.Raw("1"))), want: `{"x": 1}`}, + {name: "tuple singleton", token: py.TupleLiteral(py.ID("item")), want: "(item,)"}, + {name: "list comprehension", token: py.ListComp(py.ID("x").Op("*").Raw("2"), py.ForClause(py.ID("x"), py.ID("items")), py.IfClause(py.ID("x").Op(">").Raw("0"))), want: "[x * 2 for x in items if x > 0]"}, + {name: "async generator", token: py.GeneratorExpr(py.ID("x"), py.AsyncForClause(py.ID("x"), py.ID("items"))), want: "(x async for x in items)"}, + {name: "dict comprehension", token: py.DictComp(py.ID("x"), py.ID("x").Op("**").Raw("2"), py.ForClause(py.ID("x"), py.ID("values"))), want: "{x: x ** 2 for x in values}"}, + {name: "f-string", token: py.FString(py.FStringText("hello {"), py.FStringExpr(py.ID("name")), py.FStringText("}")), want: `f"hello {{{name}}}"`}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := render(t, test.token); got != test.want { + t.Fatalf("render() = %q, want %q", got, test.want) + } + }) + } +} + +func TestRenderBuiltinTypes(t *testing.T) { + tests := []struct { + name string + token types.Token + want string + }{ + {name: "any", token: py.Any(), want: "Any"}, + {name: "bool", token: py.Bool(), want: "bool"}, + {name: "bytes", token: py.Bytes(), want: "bytes"}, + {name: "bytearray", token: py.ByteArray(), want: "bytearray"}, + {name: "complex", token: py.Complex(), want: "complex"}, + {name: "dict", token: py.DictType(py.String(), py.Int()), want: "dict[str, int]"}, + {name: "float", token: py.Float(), want: "float"}, + {name: "frozenset", token: py.FrozenSetType(py.Int()), want: "frozenset[int]"}, + {name: "int", token: py.Int(), want: "int"}, + {name: "list", token: py.ListType(py.String()), want: "list[str]"}, + {name: "memoryview", token: py.MemoryView(), want: "memoryview"}, + {name: "object", token: py.ObjectType(), want: "object"}, + {name: "range", token: py.Range(), want: "range"}, + {name: "set", token: py.SetType(py.Int()), want: "set[int]"}, + {name: "tuple", token: py.TupleType(py.String(), py.Int()), want: "tuple[str, int]"}, + {name: "variadic tuple", token: py.TupleType(py.Int(), py.Ellipsis()), want: "tuple[int, ...]"}, + {name: "type", token: py.TypeType(), want: "type"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := render(t, test.token); got != test.want { + t.Fatalf("render() = %q, want %q", got, test.want) + } + }) + } + + if got, want := render(t, py.DictType(py.String(), py.ListType(py.Int()))), "dict[str, list[int]]"; got != want { + t.Fatalf("nested generic type = %q, want %q", got, want) + } + + got, want := render(t, py.ID("value").Colon().String()), "value: str" + if got != want { + t.Fatalf("chainable String() = %q, want %q", got, want) + } +} + +func TestRenderEscapingOperatorsAndRaw(t *testing.T) { + if got, want := render(t, py.Text("quote: \"; slash: \\; line:\n")), "\"quote: \\\"; slash: \\\\; line:\\n\""; got != want { + t.Fatalf("string = %q, want %q", got, want) + } + if got, want := render(t, py.ID("变量").Op("and").Op("not").ID("ready")), "变量 and not ready"; got != want { + t.Fatalf("word operators = %q, want %q", got, want) + } + if got, want := render(t, py.Raw("first\n second")), "first\n second"; got != want { + t.Fatalf("Raw = %q, want %q", got, want) + } + if got, want := render(t, py.Comment("first\nsecond")), "# first\n# second\n"; got != want { + t.Fatalf("comment = %q, want %q", got, want) + } +} + +func TestValidationAndRenderErrors(t *testing.T) { + for _, token := range []interface{ Render(io.Writer) error }{ + py.ID("class"), + py.ID("not-valid"), + py.Op("???"), + py.ListComp(py.ID("item")), + py.FString(py.FStringExpr(nil)), + py.Def("run").Block(nil), + } { + var output bytes.Buffer + if err := token.Render(&output); err == nil { + t.Errorf("Render(%T) succeeded, want error", token) + } + } + + var output bytes.Buffer + if err := py.Render(&output, py.ID("value")); err != nil { + t.Fatal(err) + } + if got := output.String(); got != "value" { + t.Fatalf("package Render() = %q, want %q", got, "value") + } + + wantErr := errors.New("write failed") + if err := py.Render(errorWriter{err: wantErr}, py.ID("value")); !errors.Is(err, wantErr) { + t.Fatalf("Render() error = %v, want %v", err, wantErr) + } + if err := py.Render(shortWriter{}, py.Comment("comment")); !errors.Is(err, io.ErrShortWrite) { + t.Fatalf("Render() short-write error = %v, want %v", err, io.ErrShortWrite) + } +} + +type errorWriter struct{ err error } + +func (v errorWriter) Write([]byte) (int, error) { return 0, v.err } + +type shortWriter struct{} + +func (shortWriter) Write([]byte) (int, error) { return 0, nil } diff --git a/python/suite.go b/python/suite.go new file mode 100644 index 0000000..342ee67 --- /dev/null +++ b/python/suite.go @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "errors" + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type suite struct{ body []types.Token } + +func (v *suite) Render(w io.Writer) error { + iw := gen.IndentedWith(w, " ") + if err := writeString(iw, ":"); err != nil { + return err + } + if len(v.body) == 0 { + if err := writeString(iw, " pass\n"); err != nil { + return err + } + return nil + } + if err := iw.EnsureNewline(); err != nil { + return err + } + iw.Push() + for _, token := range v.body { + if token == nil { + iw.Pop() + return errors.New("python suite contains a nil statement") + } + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + if err := gen.Render(iw, token); err != nil { + iw.Pop() + return err + } + } + if err := iw.EnsureNewline(); err != nil { + iw.Pop() + return err + } + iw.Pop() + return nil +} + +func (v *suite) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindColon, Text: ":"}, + Last: gen.Style{Kind: gen.KindLine, Text: "\n"}, + } +} + +type pythonComment struct{ text string } + +func (v *pythonComment) Render(w io.Writer) error { + lines := splitLines(v.text) + for _, text := range lines { + if err := writeString(w, "#"); err != nil { + return err + } + if text != "" && text[0] != ' ' && text[0] != '\t' { + if err := writeString(w, " "); err != nil { + return err + } + } + if err := writeString(w, text+"\n"); err != nil { + return err + } + } + return nil +} + +func (v *pythonComment) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindComment, Text: v.text} + return gen.Layout{First: style, Last: gen.Style{Kind: gen.KindLine, Text: "\n"}} +} + +func splitLines(value string) []string { + if value == "" { + return []string{""} + } + var lines []string + for len(value) > 0 { + index := -1 + for i, r := range value { + if r == '\n' { + index = i + break + } + } + if index < 0 { + lines = append(lines, value) + break + } + line := value[:index] + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + lines = append(lines, line) + value = value[index+1:] + } + return lines +} + +// Block creates a Python suite that appends a colon and indents its body. +func (v *Tokens) Block(body ...types.Token) *Tokens { return v.Join(block(body...)) } + +// Block creates a Python suite that appends a colon and indents its body. +func Block(body ...types.Token) *Tokens { return create().Block(body...) } + +// Comment creates a Python comment, prefixing every line with #. +func (v *Tokens) Comment(text string) *Tokens { return v.Join(comment(text)) } + +// Comment creates a Python comment, prefixing every line with #. +func Comment(text string) *Tokens { return create().Comment(text) } + +// Line appends an explicit line break. +func (v *Tokens) Line() *Tokens { return v.Join(line()) } + +// Line creates a token sequence containing an explicit line break. +func Line() *Tokens { return create().Line() } diff --git a/python/token_collections.go b/python/token_collections.go new file mode 100644 index 0000000..cbb554e --- /dev/null +++ b/python/token_collections.go @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "fmt" + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// ListLiteral creates a Python list literal. +func (v *Tokens) ListLiteral(values ...types.Token) *Tokens { + return v.Join(&models.SquareBracket{D: values}) +} + +// ListLiteral creates a Python list literal. +func ListLiteral(values ...types.Token) *Tokens { return create().ListLiteral(values...) } + +// DictLiteral creates a Python dictionary literal. +func (v *Tokens) DictLiteral(entries ...types.Token) *Tokens { + return v.Join(&dictLiteral{entries: entries}) +} + +// DictLiteral creates a Python dictionary literal. +func DictLiteral(entries ...types.Token) *Tokens { return create().DictLiteral(entries...) } + +// SetLiteral creates a Python set literal. An empty set is emitted as set(). +func (v *Tokens) SetLiteral(values ...types.Token) *Tokens { + return v.Join(&setLiteral{values: values}) +} + +// SetLiteral creates a Python set literal. An empty set is emitted as set(). +func SetLiteral(values ...types.Token) *Tokens { return create().SetLiteral(values...) } + +// ListComp creates a list comprehension from an expression and clauses. +func (v *Tokens) ListComp(expression types.Token, clauses ...types.Token) *Tokens { + return v.Join(&comprehension{open: "[", close: "]", expression: expression, clauses: clauses}) +} + +// ListComp creates a list comprehension from an expression and clauses. +func ListComp(expression types.Token, clauses ...types.Token) *Tokens { + return create().ListComp(expression, clauses...) +} + +// SetComp creates a set comprehension from an expression and clauses. +func (v *Tokens) SetComp(expression types.Token, clauses ...types.Token) *Tokens { + return v.Join(&comprehension{open: "{", close: "}", expression: expression, clauses: clauses}) +} + +// SetComp creates a set comprehension from an expression and clauses. +func SetComp(expression types.Token, clauses ...types.Token) *Tokens { + return create().SetComp(expression, clauses...) +} + +// DictComp creates a dictionary comprehension from key/value expressions and clauses. +func (v *Tokens) DictComp(key, value types.Token, clauses ...types.Token) *Tokens { + return v.Join(&dictComprehension{key: key, value: value, clauses: clauses}) +} + +// DictComp creates a dictionary comprehension from key/value expressions and clauses. +func DictComp(key, value types.Token, clauses ...types.Token) *Tokens { + return create().DictComp(key, value, clauses...) +} + +// GeneratorExpr creates a generator expression from an expression and clauses. +func (v *Tokens) GeneratorExpr(expression types.Token, clauses ...types.Token) *Tokens { + return v.Join(&comprehension{open: "(", close: ")", expression: expression, clauses: clauses}) +} + +// GeneratorExpr creates a generator expression from an expression and clauses. +func GeneratorExpr(expression types.Token, clauses ...types.Token) *Tokens { + return create().GeneratorExpr(expression, clauses...) +} + +// ForClause creates the for-in clause used by comprehensions. +func ForClause(target, iterable types.Token) types.Token { + return &forClause{target: target, iterable: iterable} +} + +// AsyncForClause creates an asynchronous for-in clause used by comprehensions. +func AsyncForClause(target, iterable types.Token) types.Token { + return &forClause{target: target, iterable: iterable, async: true} +} + +// IfClause creates a filter clause used by comprehensions. +func IfClause(condition types.Token) types.Token { return &ifClause{condition: condition} } + +type dictLiteral struct{ entries []types.Token } + +func (v *dictLiteral) Render(w io.Writer) error { + if _, err := io.WriteString(w, "{"); err != nil { + return err + } + if err := renderCommaSeparated(w, v.entries); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} + +func (v *dictLiteral) RenderLayout() gen.Layout { + return containerLayout(gen.KindBlockOpen, gen.KindBlockClose) +} + +type setLiteral struct{ values []types.Token } + +func (v *setLiteral) Render(w io.Writer) error { + if len(v.values) == 0 { + _, err := io.WriteString(w, "set()") + return err + } + if _, err := io.WriteString(w, "{"); err != nil { + return err + } + if err := renderCommaSeparated(w, v.values); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} + +func (v *setLiteral) RenderLayout() gen.Layout { + if len(v.values) == 0 { + style := gen.Style{Kind: gen.KindWord, Text: "set", CanEndExpression: true} + return gen.Layout{First: style, Last: gen.Style{Kind: gen.KindCloseParen}} + } + return containerLayout(gen.KindBlockOpen, gen.KindBlockClose) +} + +type comprehension struct { + open string + close string + expression types.Token + clauses []types.Token +} + +func (v *comprehension) Render(w io.Writer) error { + if v.expression == nil || len(v.clauses) == 0 { + return fmt.Errorf("comprehension requires an expression and at least one clause") + } + return renderContainer(w, v.open, v.close, append([]types.Token{v.expression}, v.clauses...)) +} + +func (v *comprehension) RenderLayout() gen.Layout { + return containerLayoutFor(v.open, v.close) +} + +type dictComprehension struct { + key types.Token + value types.Token + clauses []types.Token +} + +func (v *dictComprehension) Render(w io.Writer) error { + if v.key == nil || v.value == nil || len(v.clauses) == 0 { + return fmt.Errorf("dictionary comprehension requires key, value, and at least one clause") + } + entries := []types.Token{keyed(v.key, v.value)} + entries = append(entries, v.clauses...) + return renderContainer(w, "{", "}", entries) +} + +func (v *dictComprehension) RenderLayout() gen.Layout { + return containerLayout(gen.KindBlockOpen, gen.KindBlockClose) +} + +type forClause struct { + target types.Token + iterable types.Token + async bool +} + +func (v *forClause) Render(w io.Writer) error { + if v.target == nil || v.iterable == nil { + return fmt.Errorf("comprehension for clause requires target and iterable") + } + if v.async { + return gen.Render(w, []types.Token{keyword("async"), keyword("for"), v.target, keyword("in"), v.iterable}) + } + return gen.Render(w, []types.Token{keyword("for"), v.target, keyword("in"), v.iterable}) +} + +func (v *forClause) RenderLayout() gen.Layout { + return gen.Layout{First: wordStyle("for"), Last: gen.LayoutOf([]types.Token{v.iterable}).Last} +} + +type ifClause struct{ condition types.Token } + +func (v *ifClause) Render(w io.Writer) error { + if v.condition == nil { + return fmt.Errorf("comprehension if clause requires condition") + } + return gen.Render(w, []types.Token{keyword("if"), v.condition}) +} + +func (v *ifClause) RenderLayout() gen.Layout { + return gen.Layout{First: wordStyle("if"), Last: gen.LayoutOf([]types.Token{v.condition}).Last} +} + +func renderCommaSeparated(w io.Writer, values []types.Token) error { + for index, value := range values { + if value == nil { + return fmt.Errorf("container contains a nil value") + } + if index > 0 { + if _, err := io.WriteString(w, ", "); err != nil { + return err + } + } + if err := value.Render(w); err != nil { + return err + } + } + return nil +} + +func renderContainer(w io.Writer, open, close string, values []types.Token) error { + if _, err := io.WriteString(w, open); err != nil { + return err + } + if err := gen.Render(w, values); err != nil { + return err + } + _, err := io.WriteString(w, close) + return err +} + +func containerLayout(first, last gen.Kind) gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: first}, + Last: gen.Style{Kind: last}, + } +} + +func containerLayoutFor(open, close string) gen.Layout { + first, last := gen.KindUnknown, gen.KindUnknown + switch open { + case "[": + first = gen.KindOpenSquare + case "(": + first = gen.KindOpenParen + case "{": + first = gen.KindBlockOpen + } + switch close { + case "]": + last = gen.KindCloseBracket + case ")": + last = gen.KindCloseParen + case "}": + last = gen.KindBlockClose + } + return containerLayout(first, last) +} diff --git a/python/token_control.go b/python/token_control.go new file mode 100644 index 0000000..fc117b1 --- /dev/null +++ b/python/token_control.go @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import "go.osspkg.com/gogen/types" + +// If appends an if header; call Block to add its suite. +func (v *Tokens) If(condition types.Token) *Tokens { return v.Join(keyword("if"), condition) } + +// If creates an if header. +func If(condition types.Token) *Tokens { return create().If(condition) } + +// Elif appends an elif header. +func (v *Tokens) Elif(condition types.Token) *Tokens { return v.Join(keyword("elif"), condition) } + +// Elif creates an elif header. +func Elif(condition types.Token) *Tokens { return create().Elif(condition) } + +// Else appends an else header. +func (v *Tokens) Else() *Tokens { return v.Join(keyword("else")) } + +// Else creates an else header. +func Else() *Tokens { return create().Else() } + +// ForEach appends a for-in header. +func (v *Tokens) ForEach(target, iterable types.Token) *Tokens { + return v.Join(keyword("for"), target, keyword("in"), iterable) +} + +// ForEach creates a for-in header. +func ForEach(target, iterable types.Token) *Tokens { return create().ForEach(target, iterable) } + +// AsyncForEach appends an async for-in header. +func (v *Tokens) AsyncForEach(target, iterable types.Token) *Tokens { + return v.Join(keyword("async"), keyword("for"), target, keyword("in"), iterable) +} + +// AsyncForEach creates an async for-in header. +func AsyncForEach(target, iterable types.Token) *Tokens { + return create().AsyncForEach(target, iterable) +} + +// While appends a while header. +func (v *Tokens) While(condition types.Token) *Tokens { return v.Join(keyword("while"), condition) } + +// While creates a while header. +func While(condition types.Token) *Tokens { return create().While(condition) } + +// With appends a with header for one or more context expressions. +func (v *Tokens) With(contexts ...types.Token) *Tokens { + return v.Join(keyword("with"), list(contexts...)) +} + +// With creates a with header for one or more context expressions. +func With(contexts ...types.Token) *Tokens { return create().With(contexts...) } + +// AsyncWith appends an async with header. +func (v *Tokens) AsyncWith(contexts ...types.Token) *Tokens { + return v.Join(keyword("async"), keyword("with"), list(contexts...)) +} + +// AsyncWith creates an async with header. +func AsyncWith(contexts ...types.Token) *Tokens { return create().AsyncWith(contexts...) } + +// Try appends a try header. +func (v *Tokens) Try() *Tokens { return v.Join(keyword("try")) } + +// Try creates a try header. +func Try() *Tokens { return create().Try() } + +// Except appends an except header, optionally with an exception type. +func (v *Tokens) Except(exception ...types.Token) *Tokens { + v.Join(keyword("except")) + if len(exception) > 0 { + v.Join(list(exception...)) + } + return v +} + +// Except creates an except header, optionally with an exception type. +func Except(exception ...types.Token) *Tokens { return create().Except(exception...) } + +// Finally appends a finally header. +func (v *Tokens) Finally() *Tokens { return v.Join(keyword("finally")) } + +// Finally creates a finally header. +func Finally() *Tokens { return create().Finally() } + +// Match appends a match header. +func (v *Tokens) Match(subject types.Token) *Tokens { return v.Join(keyword("match"), subject) } + +// Match creates a match header. +func Match(subject types.Token) *Tokens { return create().Match(subject) } + +// Case appends a case header. +func (v *Tokens) Case(pattern types.Token) *Tokens { return v.Join(keyword("case"), pattern) } + +// Case creates a case header. +func Case(pattern types.Token) *Tokens { return create().Case(pattern) } + +// Return appends the return keyword. +func (v *Tokens) Return() *Tokens { return v.Join(keyword("return")) } + +// Return creates a return statement token sequence. +func Return() *Tokens { return create().Return() } + +// Yield appends the yield keyword. +func (v *Tokens) Yield() *Tokens { return v.Join(keyword("yield")) } + +// Yield creates a yield expression token sequence. +func Yield() *Tokens { return create().Yield() } + +// Raise appends the raise keyword. +func (v *Tokens) Raise() *Tokens { return v.Join(keyword("raise")) } + +// Raise creates a raise statement token sequence. +func Raise() *Tokens { return create().Raise() } + +// Pass appends the pass keyword. +func (v *Tokens) Pass() *Tokens { return v.Join(keyword("pass")) } + +// Pass creates a pass statement token sequence. +func Pass() *Tokens { return create().Pass() } + +// Break appends the break keyword. +func (v *Tokens) Break() *Tokens { return v.Join(keyword("break")) } + +// Break creates a break statement token sequence. +func Break() *Tokens { return create().Break() } + +// Continue appends the continue keyword. +func (v *Tokens) Continue() *Tokens { return v.Join(keyword("continue")) } + +// Continue creates a continue statement token sequence. +func Continue() *Tokens { return create().Continue() } + +// Assert appends the assert keyword. +func (v *Tokens) Assert() *Tokens { return v.Join(keyword("assert")) } + +// Assert creates an assert statement token sequence. +func Assert() *Tokens { return create().Assert() } + +// Del appends the del keyword. +func (v *Tokens) Del() *Tokens { return v.Join(keyword("del")) } + +// Del creates a del statement token sequence. +func Del() *Tokens { return create().Del() } + +// Global appends the global keyword. +func (v *Tokens) Global() *Tokens { return v.Join(keyword("global")) } + +// Global creates a global declaration token sequence. +func Global() *Tokens { return create().Global() } + +// Nonlocal appends the nonlocal keyword. +func (v *Tokens) Nonlocal() *Tokens { return v.Join(keyword("nonlocal")) } + +// Nonlocal creates a nonlocal declaration token sequence. +func Nonlocal() *Tokens { return create().Nonlocal() } + +// Await appends the await keyword. +func (v *Tokens) Await() *Tokens { return v.Join(keyword("await")) } + +// Await creates an await expression token sequence. +func Await() *Tokens { return create().Await() } + +// As appends the as keyword. +func (v *Tokens) As() *Tokens { return v.Join(keyword("as")) } + +// As creates an as-clause token sequence. +func As() *Tokens { return create().As() } + +// Arrow appends the function return annotation arrow. +func (v *Tokens) Arrow() *Tokens { return v.Join(operation("->")) } + +// Arrow creates the function return annotation arrow. +func Arrow() *Tokens { return create().Arrow() } + +// Colon appends a colon punctuation token. +func (v *Tokens) Colon() *Tokens { return v.Join(operation(":")) } + +// Colon creates a colon punctuation token. +func Colon() *Tokens { return create().Colon() } + +// Comma appends a comma punctuation token. +func (v *Tokens) Comma() *Tokens { return v.Join(operation(",")) } + +// Comma creates a comma punctuation token. +func Comma() *Tokens { return create().Comma() } diff --git a/python/token_declarations.go b/python/token_declarations.go new file mode 100644 index 0000000..e0d945c --- /dev/null +++ b/python/token_declarations.go @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "fmt" + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +// Import appends an import statement for a dotted module path. +func (v *Tokens) Import(module string) *Tokens { return v.Join(keyword("import"), raw(module)) } + +// Import creates an import statement for a dotted module path. +func Import(module string) *Tokens { return create().Import(module) } + +// ImportAs appends an import statement with an alias. +func (v *Tokens) ImportAs(module, alias string) *Tokens { + return v.Join(keyword("import"), raw(module), keyword("as"), identifier(alias)) +} + +// ImportAs creates an import statement with an alias. +func ImportAs(module, alias string) *Tokens { return create().ImportAs(module, alias) } + +// FromImport appends a from-import statement with optional names. +func (v *Tokens) FromImport(module string, names ...types.Token) *Tokens { + v.Join(keyword("from"), raw(module), keyword("import")) + if len(names) > 0 { + v.Join(list(names...)) + } + return v +} + +// FromImport creates a from-import statement with optional names. +func FromImport(module string, names ...types.Token) *Tokens { + return create().FromImport(module, names...) +} + +// Def appends a function declaration header; call Block to add its suite. +func (v *Tokens) Def(name string) *Tokens { return v.Join(keyword("def"), identifier(name)) } + +// Def creates a function declaration header. +func Def(name string) *Tokens { return create().Def(name) } + +// AsyncDef appends an asynchronous function declaration header. +func (v *Tokens) AsyncDef(name string) *Tokens { + return v.Join(keyword("async"), keyword("def"), identifier(name)) +} + +// AsyncDef creates an asynchronous function declaration header. +func AsyncDef(name string) *Tokens { return create().AsyncDef(name) } + +// Class appends a class declaration header with optional base classes. +func (v *Tokens) Class(name string, bases ...types.Token) *Tokens { + v.Join(keyword("class"), identifier(name)) + if len(bases) > 0 { + v.Join(brackets(bases...)) + } + return v +} + +// Class creates a class declaration header with optional base classes. +func Class(name string, bases ...types.Token) *Tokens { return create().Class(name, bases...) } + +// Decorator appends a decorator expression followed by a line break. +func (v *Tokens) Decorator(expression types.Token) *Tokens { + return v.Join(&decorator{expression: expression}) +} + +// Decorator creates a decorator expression followed by a line break. +func Decorator(expression types.Token) *Tokens { return create().Decorator(expression) } + +type decorator struct{ expression types.Token } + +func (v *decorator) Render(w io.Writer) error { + if v.expression == nil { + return fmt.Errorf("decorator expression is nil") + } + if err := writeString(w, "@"); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + err := writeString(w, "\n") + return err +} + +func (v *decorator) RenderLayout() gen.Layout { + return gen.Layout{First: gen.Style{Kind: gen.KindPrefixOperator}, Last: gen.Style{Kind: gen.KindLine}} +} diff --git a/python/token_expressions.go b/python/token_expressions.go new file mode 100644 index 0000000..2dbf34f --- /dev/null +++ b/python/token_expressions.go @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +// ID appends a validated Python identifier. +func (v *Tokens) ID(name string) *Tokens { return v.Join(identifier(name)) } + +// ID creates a token sequence containing a validated Python identifier. +func ID(name string) *Tokens { return create().ID(name) } + +// Raw appends source text verbatim without validating it. +func (v *Tokens) Raw(source string) *Tokens { return v.Join(raw(source)) } + +// Raw creates a token sequence containing source text verbatim. +func Raw(source string) *Tokens { return create().Raw(source) } + +// Text appends an escaped Python string literal. +func (v *Tokens) Text(value string) *Tokens { return v.Join(text(value)) } + +// Text creates a token sequence containing an escaped Python string literal. +func Text(value string) *Tokens { return create().Text(value) } + +// Op appends a supported Python operator. +func (v *Tokens) Op(op string) *Tokens { return v.Join(operation(op)) } + +// Op creates a token sequence containing a supported Python operator. +func Op(op string) *Tokens { return create().Op(op) } + +// Call appends a parenthesized call argument list. +func (v *Tokens) Call(args ...types.Token) *Tokens { return v.Join(brackets(args...)) } + +// Call creates a parenthesized call argument list. +func Call(args ...types.Token) *Tokens { return create().Call(args...) } + +// Bracket appends a parenthesized token list. +func (v *Tokens) Bracket(args ...types.Token) *Tokens { return v.Join(brackets(args...)) } + +// Bracket creates a parenthesized token list. +func Bracket(args ...types.Token) *Tokens { return create().Bracket(args...) } + +// List appends a comma-separated sequence without delimiters. +func (v *Tokens) List(args ...types.Token) *Tokens { return v.Join(list(args...)) } + +// List creates a comma-separated sequence without delimiters. +func List(args ...types.Token) *Tokens { return create().List(args...) } + +// Index appends a square-bracketed index or type argument. +func (v *Tokens) Index(index types.Token) *Tokens { return v.Join(square(index)) } + +// Index creates a square-bracketed index or type argument. +func Index(index types.Token) *Tokens { return create().Index(index) } + +// KeyValue creates a dictionary or keyword-argument entry. +func KeyValue(key, value types.Token) *Tokens { return create().Join(keyed(key, value)) } + +// Selector appends an attribute selector. +func (v *Tokens) Selector(name string) *Tokens { return v.Join(operation("."), identifier(name)) } + +// Selector creates an attribute selector. +func Selector(name string) *Tokens { return create().Selector(name) } + +// Pkg appends a dotted module or package name. +func (v *Tokens) Pkg(module string) *Tokens { + if module == "" { + return v + } + for i, part := range splitDots(module) { + if i > 0 { + v = v.Join(operation(".")) + } + v = v.Join(identifier(part)) + } + return v +} + +// Pkg creates a dotted module or package name. +func Pkg(module string) *Tokens { return create().Pkg(module) } + +// TupleLiteral creates a Python tuple literal. +func (v *Tokens) TupleLiteral(values ...types.Token) *Tokens { + return v.Join(&tupleLiteral{values: values}) +} + +// TupleLiteral creates a Python tuple literal. +func TupleLiteral(values ...types.Token) *Tokens { return create().TupleLiteral(values...) } + +// TypeUnion creates a Python 3.10 union annotation. +func (v *Tokens) TypeUnion(values ...types.Token) *Tokens { + for index, value := range values { + if index > 0 { + v = v.Op("|") + } + v = v.Join(value) + } + return v +} + +// TypeUnion creates a Python 3.10 union annotation. +func TypeUnion(values ...types.Token) *Tokens { return create().TypeUnion(values...) } + +func splitDots(value string) []string { + parts := make([]string, 0, 1) + start := 0 + for i, r := range value { + if r == '.' { + parts = append(parts, value[start:i]) + start = i + 1 + } + } + return append(parts, value[start:]) +} + +type tupleLiteral struct{ values []types.Token } + +func (v *tupleLiteral) Render(w io.Writer) error { + out := make([]types.Token, 0, len(v.values)*2+3) + out = append(out, gen.Symbol("(", gen.Style{Kind: gen.KindOpenParen})) + for index, value := range v.values { + if index > 0 { + out = append(out, gen.Symbol(",", gen.Style{Kind: gen.KindComma})) + } + out = append(out, value) + } + if len(v.values) == 1 { + out = append(out, gen.Symbol(",", gen.Style{Kind: gen.KindComma})) + } + out = append(out, gen.Symbol(")", gen.Style{Kind: gen.KindCloseParen})) + return gen.Render(w, out) +} + +func (v *tupleLiteral) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindOpenParen}, + Last: gen.Style{Kind: gen.KindCloseParen}, + } +} diff --git a/python/token_fstring.go b/python/token_fstring.go new file mode 100644 index 0000000..158e738 --- /dev/null +++ b/python/token_fstring.go @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "fmt" + "io" + "strings" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +// FString creates a Python f-string from text and expression parts. +func (v *Tokens) FString(parts ...types.Token) *Tokens { return v.Join(&fString{parts: parts}) } + +// FString creates a Python f-string from text and expression parts. +func FString(parts ...types.Token) *Tokens { return create().FString(parts...) } + +// FStringText creates an escaped text part for FString. +func FStringText(value string) types.Token { return fStringText(value) } + +// FStringExpr creates an expression part for FString. +func FStringExpr(expression types.Token) types.Token { return fStringExpr{expression: expression} } + +type fString struct{ parts []types.Token } + +func (v *fString) Render(w io.Writer) error { + if err := writeString(w, "f\""); err != nil { + return err + } + for _, part := range v.parts { + if part == nil { + return fmt.Errorf("f-string contains a nil part") + } + if err := part.Render(w); err != nil { + return err + } + } + err := writeString(w, "\"") + return err +} + +func (v *fString) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindLiteral} + return gen.Layout{First: style, Last: style} +} + +type fStringText string + +func (v fStringText) Render(w io.Writer) error { + var out strings.Builder + for _, r := range string(v) { + switch r { + case '\\': + out.WriteString("\\\\") + case '"': + out.WriteString("\\\"") + case '{': + out.WriteString("{{") + case '}': + out.WriteString("}}") + case '\n': + out.WriteString("\\n") + case '\r': + out.WriteString("\\r") + case '\t': + out.WriteString("\\t") + default: + out.WriteRune(r) + } + } + err := writeString(w, out.String()) + return err +} + +type fStringExpr struct{ expression types.Token } + +func (v fStringExpr) Render(w io.Writer) error { + if v.expression == nil { + return fmt.Errorf("f-string expression is nil") + } + if err := writeString(w, "{"); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + err := writeString(w, "}") + return err +} diff --git a/python/token_types.go b/python/token_types.go new file mode 100644 index 0000000..e7bc458 --- /dev/null +++ b/python/token_types.go @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import "go.osspkg.com/gogen/types" + +// Any appends Any, the typing module's unconstrained type annotation. +func (v *Tokens) Any() *Tokens { return v.ID("Any") } + +// Any creates an Any type annotation. Import Any from typing in generated code. +func Any() *Tokens { return create().Any() } + +// Bool appends Python's bool type. +func (v *Tokens) Bool() *Tokens { return v.ID("bool") } + +// Bool creates Python's bool type. +func Bool() *Tokens { return create().Bool() } + +// Bytes appends Python's bytes type. +func (v *Tokens) Bytes() *Tokens { return v.ID("bytes") } + +// Bytes creates Python's bytes type. +func Bytes() *Tokens { return create().Bytes() } + +// ByteArray appends Python's bytearray type. +func (v *Tokens) ByteArray() *Tokens { return v.ID("bytearray") } + +// ByteArray creates Python's bytearray type. +func ByteArray() *Tokens { return create().ByteArray() } + +// Complex appends Python's complex type. +func (v *Tokens) Complex() *Tokens { return v.ID("complex") } + +// Complex creates Python's complex type. +func Complex() *Tokens { return create().Complex() } + +// DictType appends a parameterized dict type such as dict[str, int]. +func (v *Tokens) DictType(key, value types.Token) *Tokens { + return v.ID("dict").Index(List(key, value)) +} + +// DictType creates a parameterized dict type such as dict[str, int]. +func DictType(key, value types.Token) *Tokens { return create().DictType(key, value) } + +// Float appends Python's float type. +func (v *Tokens) Float() *Tokens { return v.ID("float") } + +// Float creates Python's float type. +func Float() *Tokens { return create().Float() } + +// FrozenSetType appends a parameterized frozenset type. +func (v *Tokens) FrozenSetType(element types.Token) *Tokens { + return v.ID("frozenset").Index(element) +} + +// FrozenSetType creates a parameterized frozenset type. +func FrozenSetType(element types.Token) *Tokens { return create().FrozenSetType(element) } + +// Int appends Python's int type. +func (v *Tokens) Int() *Tokens { return v.ID("int") } + +// Int creates Python's int type. +func Int() *Tokens { return create().Int() } + +// ListType appends a parameterized list type such as list[str]. +func (v *Tokens) ListType(element types.Token) *Tokens { + return v.ID("list").Index(element) +} + +// ListType creates a parameterized list type such as list[str]. +func ListType(element types.Token) *Tokens { return create().ListType(element) } + +// MemoryView appends Python's memoryview type. +func (v *Tokens) MemoryView() *Tokens { return v.ID("memoryview") } + +// MemoryView creates Python's memoryview type. +func MemoryView() *Tokens { return create().MemoryView() } + +// ObjectType appends Python's object type. +func (v *Tokens) ObjectType() *Tokens { return v.ID("object") } + +// ObjectType creates Python's object type. +func ObjectType() *Tokens { return create().ObjectType() } + +// Range appends Python's range type. +func (v *Tokens) Range() *Tokens { return v.ID("range") } + +// Range creates Python's range type. +func Range() *Tokens { return create().Range() } + +// SetType appends a parameterized set type such as set[int]. +func (v *Tokens) SetType(element types.Token) *Tokens { + return v.ID("set").Index(element) +} + +// SetType creates a parameterized set type such as set[int]. +func SetType(element types.Token) *Tokens { return create().SetType(element) } + +// Str appends Python's str type. +func (v *Tokens) Str() *Tokens { return v.ID("str") } + +// Str creates Python's str type. +func Str() *Tokens { return create().Str() } + +// String appends Python's str type as a readable alias for Str. +func (v *Tokens) String() *Tokens { return v.Str() } + +// String creates Python's str type as a readable alias for Str. +func String() *Tokens { return create().String() } + +// TupleType appends a tuple type. Pass element types for fixed tuples or +// Ellipsis() as the second argument for a variable-length tuple. +func (v *Tokens) TupleType(elements ...types.Token) *Tokens { + v.ID("tuple") + if len(elements) > 0 { + v.Index(List(elements...)) + } + return v +} + +// TupleType creates a tuple type. Pass element types for fixed tuples or +// Ellipsis() as the second argument for a variable-length tuple. +func TupleType(elements ...types.Token) *Tokens { return create().TupleType(elements...) } + +// TypeType appends Python's type type. +func (v *Tokens) TypeType() *Tokens { return v.ID("type") } + +// TypeType creates Python's type type. +func TypeType() *Tokens { return create().TypeType() } diff --git a/python/token_values.go b/python/token_values.go new file mode 100644 index 0000000..ff0820e --- /dev/null +++ b/python/token_values.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import "go.osspkg.com/gogen/internal/gen" + +// NoneValue appends Python's None literal. +func (v *Tokens) NoneValue() *Tokens { return v.Join(keyword("None")) } + +// NoneValue creates Python's None literal. +func NoneValue() *Tokens { return create().NoneValue() } + +// True appends Python's True literal. +func (v *Tokens) True() *Tokens { return v.Join(keyword("True")) } + +// True creates Python's True literal. +func True() *Tokens { return create().True() } + +// False appends Python's False literal. +func (v *Tokens) False() *Tokens { return v.Join(keyword("False")) } + +// False creates Python's False literal. +func False() *Tokens { return create().False() } + +// Ellipsis appends Python's ellipsis literal. +func (v *Tokens) Ellipsis() *Tokens { + return v.Join(gen.Symbol("...", gen.Style{Kind: gen.KindLiteral, Text: "..."})) +} + +// Ellipsis creates Python's ellipsis literal. +func Ellipsis() *Tokens { return create().Ellipsis() } diff --git a/python/tokens.go b/python/tokens.go new file mode 100644 index 0000000..24e4b6d --- /dev/null +++ b/python/tokens.go @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package python + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// Tokens is a chainable sequence of Python source tokens. +type Tokens []types.Token + +func create() *Tokens { return &Tokens{} } + +// Render writes arg without formatting or validating the complete Python program. +func Render(w io.Writer, arg types.Token) error { return arg.Render(w) } + +// Render writes the token sequence to w without formatting it. +func (v *Tokens) Render(w io.Writer) error { return gen.Render(w, []types.Token(*v)) } + +// Unwrap returns the underlying token slice without copying it. +func (v *Tokens) Unwrap() []types.Token { return *v } + +// Join appends tokens to the sequence and returns it. +func (v *Tokens) Join(args ...types.Token) *Tokens { + *v = append(*v, args...) + return v +} + +func wordStyle(value string) gen.Style { + return gen.Style{Kind: gen.KindWord, Text: value, CanEndExpression: (languageConfig{}).CanEndExpression(value)} +} + +func keyword(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value} +} + +func identifier(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value, Verify: true} +} + +func raw(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value, Raw: true} +} + +func text(value string) types.Token { + return &models.Text[languageConfig]{C: languageConfig{}, D: value} +} + +func operation(value string) types.Token { return &models.Operation[languageConfig]{D: value} } + +func block(body ...types.Token) types.Token { return &suite{body: body} } + +func brackets(args ...types.Token) types.Token { return &models.Bracket{D: args, Brace: true} } + +func list(args ...types.Token) types.Token { return &models.Bracket{D: args} } + +func square(args ...types.Token) types.Token { return &models.SquareBracket{D: args} } + +func keyed(key, value types.Token) types.Token { return &models.KeyValue{Key: key, Value: value} } + +func comment(value string) types.Token { return &pythonComment{text: value} } + +func line() types.Token { return gen.Symbol("\n", gen.Style{Kind: gen.KindLine}) } + +func writeString(w io.Writer, value string) error { + n, err := io.WriteString(w, value) + if err != nil { + return err + } + if n != len(value) { + return io.ErrShortWrite + } + return nil +} diff --git a/skills/gogen-usage/SKILL.md b/skills/gogen-usage/SKILL.md new file mode 100644 index 0000000..91129c7 --- /dev/null +++ b/skills/gogen-usage/SKILL.md @@ -0,0 +1,24 @@ +--- +name: gogen-usage +description: Use the go.osspkg.com/gogen/golang token builder to generate, compose, render, or troubleshoot Go source in projects that depend on gogen. +--- + +# Using gogen + +Use this skill when writing Go code that builds Go source with `go.osspkg.com/gogen/golang`. It covers the public token API and its rendering behavior; it does not describe changes to gogen's internal implementation. + +## Working rules + +- Start with package-level constructors such as `Package`, `Func`, `ID`, `Text`, and `Block`. They return `*Tokens`; fluent methods append tokens and return the sequence for chaining. Use `Join` to append existing token sequences. +- Confirm a method or constructor exists before using it. The package-level functions and `*Tokens` methods overlap but are not identical. See [API reference](references/api-reference.md) for the complete lists and method-only/package-only cases. +- Use typed builders for identifiers, string literals, operators, calls, indexes, keyed elements, and common Go types. Reserve `Raw` for source fragments that must be inserted verbatim; it does not validate or escape its content. +- Build multi-line syntax with `Block`, `Line`, `ImportBlock`, and `TypeBlock`. Nested token sequences are flattened; nesting alone does not create line breaks. +- Use `Tokens.Render(w)` when readable pre-format output or a source fragment is needed. Use package-level `golang.Render(w, token)` for complete Go source; it applies `go/format` by default. Formatting checks syntax, not types. +- `SetRawMode` and `SetDefaultMode` change package-wide render behavior. Avoid toggling these modes around independent or concurrent rendering flows. Raw token contents remain verbatim in either mode. +- Propagate render errors. `ID` and `Op` validate their inputs during rendering; `Field` returns an error for an odd number of tag strings. + +## References + +- Read [API reference](references/api-reference.md) to choose the correct constructor, fluent method, or rendering path. +- Read [examples](references/examples.md) when composing declarations, struct tags, or expressions. Runnable programs live under `examples/`. +- Keep the API reference and examples aligned with the current package docs; confirm signatures with `go doc ./golang` if the library changes. diff --git a/skills/gogen-usage/agents/openai.yaml b/skills/gogen-usage/agents/openai.yaml new file mode 100644 index 0000000..8474b25 --- /dev/null +++ b/skills/gogen-usage/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Gogen Usage" + short_description: "Help with Gogen Usage tasks" diff --git a/skills/gogen-usage/examples/declarations/main.go b/skills/gogen-usage/examples/declarations/main.go new file mode 100644 index 0000000..452faaf --- /dev/null +++ b/skills/gogen-usage/examples/declarations/main.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package main + +import ( + "fmt" + "os" + + gogen "go.osspkg.com/gogen/golang" +) + +func main() { + file := gogen.Package("example"). + ImportBlock(gogen.Text("fmt")). + TypeBlock( + gogen.ID("User").Struct().Block( + gogen.Field("Name", gogen.String(), "json", "name"), + ), + ). + Join( + gogen.Func().ID("main").Bracket().Block( + gogen.Var().ID("user").Op("=").ID("User").Op("{"). + KeyValue(gogen.ID("Name"), gogen.Text("Ada")).Op("}"), + gogen.Pkg("fmt").ID("Println").Call(gogen.ID("user").Op(".").ID("Name")), + ), + ) + + if err := gogen.Render(os.Stdout, file); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/skills/gogen-usage/examples/expressions/main.go b/skills/gogen-usage/examples/expressions/main.go new file mode 100644 index 0000000..8865cac --- /dev/null +++ b/skills/gogen-usage/examples/expressions/main.go @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package main + +import ( + "bytes" + "fmt" + "os" + + gogen "go.osspkg.com/gogen/golang" +) + +func main() { + expression := gogen.ID("lookup").TypeArgs(gogen.String()).Call( + gogen.ID("items").Index(gogen.Raw("0")), + ) + + var fragment bytes.Buffer + if err := expression.Render(&fragment); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Printf("Tokens.Render: %s\n", fragment.String()) + + file := gogen.Package("example").Join( + gogen.Func().ID("lookup").Bracket( + gogen.ID("items").Slice().String(), + gogen.ID("index").Int(), + ).String().Block( + gogen.Return().ID("items").Index(gogen.ID("index")), + ), + ) + + if err := gogen.Render(os.Stdout, file); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/skills/gogen-usage/references/api-reference.md b/skills/gogen-usage/references/api-reference.md new file mode 100644 index 0000000..c1bb47c --- /dev/null +++ b/skills/gogen-usage/references/api-reference.md @@ -0,0 +1,53 @@ +# gogen Go API reference + +Import the Go adapter as `gogen "go.osspkg.com/gogen/golang"`. Constructors return `*gogen.Tokens`; fluent methods append to a sequence and return it. `*Tokens` also implements `types.Token`. + +## Choose a builder + +| Need | API | Notes | +| --- | --- | --- | +| File/package header | `Package(name)` | Appends a package declaration and line break. | +| One import | `Import(alias, path)` | Both arguments are strings; the path is quoted as a Go string literal. | +| Grouped imports | `ImportBlock(specs...)` | Each spec is `Text(path)` or `ID(alias).Text(path)`. Do not include the `import` keyword. | +| One type declaration | `Type().ID(name).Join(type)` | Compose the type keyword, name, and underlying type. | +| Grouped type declarations | `TypeBlock(specs...)` | Each spec contains the name followed by its type, e.g. `ID("Name").String()`. Do not include the `type` keyword. | +| Struct/interface body | `Struct().Block(...)`, `Interface().Block(...)` | `Block` controls braces, indentation, and line breaks. | +| Struct field and tags | `Field(name, type, key, value, ...)` | Tag arguments are alternating key/value strings. An odd count is a render error. | +| Function declaration | `Func().ID(name).Bracket(params...).Block(body...)` | `Bracket` adds a comma-separated parenthesized list; append result types before `Block`. | +| Function call | `ID(name).Call(args...)` | For package selectors use `Pkg("fmt").ID("Println").Call(...)`. For a value or field selector, add the dot explicitly: `ID("user").Op(".").ID("Name")`. | +| Index/generic arguments | `ID(name).Index(index)`, `ID(name).TypeArgs(args...)` | Both emit square-bracket syntax. | +| Keyed composite element | `KeyValue(key, value)` | Use inside a composite literal, usually between `Op("{")` and `Op("}")`. | +| String literal | `Text(value)` | Escapes and quotes the value as a Go string literal. | +| Identifier | `ID(value)` | Checks the builder's accepted identifier form when rendered. | +| Source fragment | `Raw(source)` | Inserts text verbatim; use only when token-level builders are unsuitable. | +| Operator | `Op(operator)` | Accepts supported Go operator/punctuation strings and reports an error for unsupported values. | +| Common allocation | `New(type)`, `Make(type, length, capacity)`, `Append(dst, values...)` | `Make` emits capacity only if it is greater than length. | +| Common types | `Map(key, value)`, `Slice().Type()`, `Array(n).Type()` and primitive type constructors | `Array(n)` does not validate whether `n` is a legal Go array length. | +| Block/comment/newline | `Block(...)`, `Comment(text)`, `Line()` | Use explicit structure for layout; nesting a token sequence does not itself add a newline. | +| Custom token | `types.Token` | Implement `Render(io.Writer) error`; write source and propagate writer/render errors. | + +## Complete constructor inventory + +These package-level functions start token sequences: + +- Declarations: `Package`, `Import`, `ImportBlock`, `Type`, `TypeBlock`, `Var`, `Const`. +- Functions and control flow: `Func`, `Return`, `Defer`, `Go`, `If`, `For`, `Switch`, `Select`, `Case`, `Default`. +- Calls and lists: `Call`, `Params`, `List`. +- Expressions: `ID`, `Pkg`, `Op`, `Raw`, `Text`, `Index`, `TypeArgs`, `KeyValue`. +- Composite values: `New`, `Make`, `Append`. +- Types and values: `Any`, `Nil`, `Chan`, `Interface`, `Struct`, `Slice`, `Array`, `Map`, `Bool`, `Byte`, `Rune`, `String`, `Error`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`, `Uintptr`, `Float32`, `Float64`, `Complex64`, `Complex128`. +- Layout: `Block`, `Comment`, `Line`, `Field`. +- Rendering: `Render`, `SetRawMode`, `SetDefaultMode`. + +Most constructors also have a same-named fluent method. Exceptions to remember: + +- Fluent-only methods: `Bracket`, `Break`, `Continue`, `Else`, `ElseIf`, `Fallthrough`, `Goto`, `Range`, `Join`, `Unwrap`, and `(*Tokens).Render`. +- Package-level only: `Defer`, `Go`, `Switch`, `Select`, `Params`, `Render`, `SetRawMode`, and `SetDefaultMode`. +- `Tokens` is a slice-backed builder. `Unwrap()` returns the backing slice without copying it. + +## Rendering choices + +- `token.Render(w)` on `*Tokens` emits readable token layout and does not call `go/format`. +- `gogen.Render(w, token)` applies `go/format` by default and returns render, formatting, or writer errors. It does not compile or type-check the result. +- `SetRawMode()` disables formatting for subsequent package-level `Render` calls; `SetDefaultMode()` restores it. This mode is package-wide state. +- `Raw` always preserves its own text verbatim. Raw mode controls the formatter, not the contents of a Raw token. diff --git a/skills/gogen-usage/references/examples.md b/skills/gogen-usage/references/examples.md new file mode 100644 index 0000000..9912524 --- /dev/null +++ b/skills/gogen-usage/references/examples.md @@ -0,0 +1,24 @@ +# gogen examples + +These examples exercise real package APIs. Run either standalone program from the repository root: + +```sh +go run ./skills/gogen-usage/examples/declarations +go run ./skills/gogen-usage/examples/expressions +``` + +## Build a file with grouped declarations and struct tags + +See [`examples/declarations/main.go`](../examples/declarations/main.go). It demonstrates grouped imports and type declarations, struct fields with tags, a keyed composite literal, a package selector, and formatted output. + +## Compose an expression and compare render modes + +See [`examples/expressions/main.go`](../examples/expressions/main.go). It demonstrates generic type arguments and indexing with `Tokens.Render`, then renders a complete function with `golang.Render`. + +## API usage patterns + +- Prefer `Text` for Go string literals, including import paths; it handles quoting. +- Prefer `ID` for identifier text and `Op` for supported operators. These builders validate when rendered. +- Use `KeyValue` within composite literal braces; use `Index` for an expression like `items[i]`, and `TypeArgs` for a generic instantiation like `Set[string]`. +- Use `Block` for nested statement or type bodies. Use `Line` when composing top-level declarations manually. +- Keep arbitrary `Raw` fragments small and syntactically complete. `Raw` bypasses escaping and validation, although `golang.Render` still formats the combined output by default. diff --git a/types/doc.go b/types/doc.go new file mode 100644 index 0000000..159ea9b --- /dev/null +++ b/types/doc.go @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package types defines the public interfaces used by gogen tokens. +package types diff --git a/types/token.go b/types/token.go index bac87ab..b793046 100644 --- a/types/token.go +++ b/types/token.go @@ -7,6 +7,8 @@ package types import "io" +// Token renders source text to a writer. type Token interface { + // Render writes the token's source text to w and returns any write or render error. Render(w io.Writer) error } diff --git a/typescript/README.md b/typescript/README.md new file mode 100644 index 0000000..8061488 --- /dev/null +++ b/typescript/README.md @@ -0,0 +1,115 @@ +# TypeScript and TSX builders + +The `go.osspkg.com/gogen/typescript` package builds TypeScript and TSX source from Go. It provides package-level constructors and chainable `*Tokens` methods, backed by the shared gogen token renderer. + +```sh +go get go.osspkg.com/gogen/typescript +``` + +## Build and render TypeScript + +Compose declarations from builders, then render the token sequence to an `io.Writer`: + +```go +package main + +import ( + "bytes" + "fmt" + + ts "go.osspkg.com/gogen/typescript" +) + +func main() { + userType := ts.Export().Type().ID("User").Op("=").Block( + ts.ID("id").Colon().String().Semicolon(), + ts.ID("name").Colon().String().Semicolon(), + ).Semicolon() + file := ts.Import("node:fs/promises", ts.ImportNames(ts.ID("readFile"))).Line().Join(userType) + + var source bytes.Buffer + if err := ts.Render(&source, file); err != nil { + panic(err) + } + fmt.Print(source.String()) +} +``` + +Output: + +```typescript +import { readFile } from "node:fs/promises"; +export type User = { + id: string; + name: string; +}; +``` + +`Tokens.Render(w)` and `typescript.Render(w, token)` produce readable token output without formatting, compiling, or type-checking it. `Block` creates explicit indented layout; nested token sequences do not imply line breaks. Use `.ts` when the generated file has no JSX. + +## Build TSX + +TSX nodes are structural tokens. The API does not depend on React or another framework. Use `JSXText` for literal text and `JSXExpr` for expressions; use the attribute builder that matches the value kind: + +```go +package main + +import ( + "bytes" + "fmt" + + "go.osspkg.com/gogen/types" + ts "go.osspkg.com/gogen/typescript" +) + +func main() { + node := ts.JSXElement( + ts.ID("button"), + []types.Token{ + ts.JSXAttribute("type", "button"), + ts.JSXAttributeExpr("disabled", ts.ID("isDisabled")), + ts.JSXSpreadAttribute(ts.ID("props")), + ts.JSXBooleanAttribute("autoFocus"), + }, + ts.JSXText("Save "), + ts.JSXExpr(ts.ID("label")), + ) + var source bytes.Buffer + if err := ts.Render(&source, node); err != nil { + panic(err) + } + fmt.Print(source.String()) +} +``` + +The rendered node is: + +```tsx + +``` + +Use `JSXFragment(children...)` for a fragment. An element with no children is rendered as self-closing JSX. Text and quoted attribute values are escaped for JSX; expression tokens are rendered inside braces. Write generated code containing JSX to a `.tsx` file. This package does not run a formatter; invoke TypeScript or Prettier separately when needed. + +## Builder reference + +Builders are available both as package-level functions and as chainable `*Tokens` methods, except where noted. `TemplateText` and `TemplateExpr` return tokens to pass to `Template`. + +| Area | Builders | Notes | +| --- | --- | --- | +| Modules | `Import`, `ImportType`, `ImportNames`, `ImportNamespace`, `Export`, `ExportDefault`, `ExportNames`, `ExportFrom`, `From` | `From` is fluent-only. Supports side-effect, default, named, namespace, type-only, and re-export declarations. Use `ID(name).As().ID(alias)` for aliases. | +| Declarations | `Const`, `Let`, `Var`, `Function`, `Type`, `Interface`, `Class`, `Namespace`, `Async`, `Await`, `Return`, `Throw`, `Extends`, `Implements`, `New`, `This` | Compose declarations and common statement keywords. Use `Bracket` for parameters, `Colon` for annotations, and `Block` for bodies. | +| Class modifiers | `Public`, `Private`, `Protected`, `Static`, `Readonly`, `Abstract`, `Override`, `Constructor` | Compose common class members and constructors. | +| Control flow | `If`, `Else`, `ElseIf`, `For`, `ForOf`, `ForIn`, `While`, `Do`, `Switch`, `Case`, `Default`, `Break`, `Continue`, `Try`, `Catch`, `Finally`, `Of` | `ForOf` and `ForIn` create loops with a `const` binding. | +| Expressions | `ID`, `Pkg`, `Raw`, `Text`, `Op`, `Call`, `Bracket`, `List`, `Index`, `TypeArgs`, `Selector`, `OptionalChain`, `NewCall`, `KeyValue`, `Spread`, `Arrow`, `As`, `Optional`, `NonNull` | `Text` creates an escaped string literal; `Raw` inserts caller-provided source verbatim. | +| Punctuation | `Comma`, `Colon`, `Semicolon` | Append the corresponding syntax separators. | +| Values and types | `ObjectLiteral`, `ArrayLiteral`, `Template`, `TemplateText`, `TemplateExpr`, `Any`, `Unknown`, `Never`, `Void`, `String`, `Number`, `Boolean`, `BigInt`, `Symbol`, `ObjectType`, `Null`, `Undefined`, `True`, `False`, `ArrayType`, `RecordType`, `MapType`, `SetType`, `PromiseType`, `Union`, `Intersection` | Compose object/array values, template strings, primitive and utility types, and unions or intersections. | +| TSX | `JSXElement`, `JSXFragment`, `JSXAttribute`, `JSXAttributeExpr`, `JSXBooleanAttribute`, `JSXSpreadAttribute`, `JSXText`, `JSXExpr` | Build elements, fragments, attributes, text nodes, and expression children. | +| Layout and output | `Block`, `Comment`, `Line`, `Join`, `Render`, `Unwrap` | Set explicit layout, combine token sequences, render source, or access the underlying token slice. | + +## Rendering errors and raw source + +Rendering returns errors from the destination writer and from invalid builder input. `ID` validates identifier syntax, and `Op` rejects operators outside the adapter's supported set. TSX rendering rejects invalid attribute names, nil tags, nil expressions, and nil children. An element with no children is valid and self-closing. + +`Raw(source)` writes the supplied source verbatim. It does not parse or validate it, so keep raw input under the caller's control. `Text(value)` emits an escaped JavaScript string literal; `JSXText(value)` and `JSXAttribute(name, value)` escape literal values for their JSX contexts. + +For the repository overview and other language guides, see the [root README](../README.md), [Go guide](../golang/README.md), and [Python guide](../python/README.md). The package's exported symbols are also documented on [pkg.go.dev](https://pkg.go.dev/go.osspkg.com/gogen/typescript). diff --git a/typescript/config.go b/typescript/config.go new file mode 100644 index 0000000..756b87e --- /dev/null +++ b/typescript/config.go @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "fmt" + "strings" + "unicode" + + "go.osspkg.com/gogen/internal/config" +) + +var _ config.Config = languageConfig{} + +type languageConfig struct{} + +func (languageConfig) OperationAvailable(op string) bool { + switch op { + case "+", "-", "*", "/", "%", "**", "++", "--", "==", "===", "!=", "!==", "<", ">", "<=", ">=", "=", "+=", "-=", "*=", "/=", "%=", "**=", "&&", "||", "??", "&&=", "||=", "??=", "!", "~", "&", "|", "^", "<<", ">>", ">>>", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "=>", "?", ":", ";", ",", ".", "?.", "...", "(", ")", "[", "]", "{", "}": + return true + default: + return false + } +} + +func (languageConfig) OperationKind(op string) config.TokenKind { + switch op { + case ".", "?.": + return config.TokenDot + case ",": + return config.TokenComma + case ":": + return config.TokenColon + case ";": + return config.TokenSemicolon + case "(": + return config.TokenOpenParen + case ")": + return config.TokenCloseParen + case "[": + return config.TokenOpenSquare + case "]": + return config.TokenCloseSquare + case "{": + return config.TokenBlockOpen + case "}": + return config.TokenBlockClose + case "++", "--": + return config.TokenPostfixOperator + case "!", "~", "+", "-", "*", "&", "...": + return config.TokenPrefixOperator + default: + return config.TokenDefault + } +} + +func (languageConfig) RawKind(text string, verified bool) config.TokenKind { + if verified { + return config.TokenDefault + } + switch text { + case "]": + return config.TokenCloseSquare + case "?.": + return config.TokenDot + default: + return config.TokenDefault + } +} + +func (languageConfig) IsIdentifier(text string) bool { + if text == "" { + return false + } + for index, r := range text { + if isIdentifierStart(r) { + continue + } + if index == 0 || !isIdentifierContinue(r) { + return false + } + } + return true +} + +func isIdentifierStart(r rune) bool { + return r == '$' || r == '_' || unicode.IsLetter(r) || unicode.In(r, unicode.Nl) +} + +func isIdentifierContinue(r rune) bool { + return isIdentifierStart(r) || unicode.IsDigit(r) || unicode.In(r, unicode.Mn, unicode.Mc, unicode.Pc) || r == '\u200c' || r == '\u200d' +} + +func (languageConfig) CanEndExpression(word string) bool { + switch strings.TrimSpace(word) { + case "as", "asserts", "abstract", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "finally", "for", "function", "if", "implements", "import", "in", "infer", "instanceof", "interface", "keyof", "let", "namespace", "new", "private", "protected", "public", "readonly", "return", "override", "constructor", "satisfies", "static", "switch", "throw", "try", "type", "typeof", "var", "void", "while", "with": + return false + default: + return true + } +} + +func (languageConfig) QuoteString(value string) string { + var quoted strings.Builder + quoted.Grow(len(value) + 2) + quoted.WriteByte('"') + for _, r := range value { + switch r { + case '\\', '"': + quoted.WriteByte('\\') + quoted.WriteRune(r) + case '\b': + quoted.WriteString("\\b") + case '\f': + quoted.WriteString("\\f") + case '\n': + quoted.WriteString("\\n") + case '\r': + quoted.WriteString("\\r") + case '\t': + quoted.WriteString("\\t") + case '\u2028': + quoted.WriteString("\\u2028") + case '\u2029': + quoted.WriteString("\\u2029") + default: + if r < 0x20 || r == 0x7f { + quoted.WriteString(fmt.Sprintf("\\x%02x", r)) + } else { + quoted.WriteRune(r) + } + } + } + quoted.WriteByte('"') + return quoted.String() +} + +func (languageConfig) CommentSingle() config.OpenClose { + return config.OpenClose{Open: "//", Close: "\n", SpaceAfterOpenWhenNeeded: true} +} + +func (languageConfig) CommentMulti() config.OpenClose { + return config.OpenClose{Open: "/*\n", Close: "\n*/\n"} +} diff --git a/typescript/doc.go b/typescript/doc.go new file mode 100644 index 0000000..c31ef12 --- /dev/null +++ b/typescript/doc.go @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package typescript builds TypeScript and TSX source from composable tokens. +// Package-level builders create token sequences and fluent methods append to +// them. Render writes readable token layout without formatting, compiling, or +// type-checking the output. +package typescript diff --git a/typescript/example_test.go b/typescript/example_test.go new file mode 100644 index 0000000..dcf2638 --- /dev/null +++ b/typescript/example_test.go @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript_test + +import ( + "bytes" + "fmt" + + "go.osspkg.com/gogen/types" + ts "go.osspkg.com/gogen/typescript" +) + +func ExampleRender() { + file := ts.ExportDefault( + ts.Function().ID("Greeting").Bracket( + ts.ID("name").Colon().String(), + ).Block( + ts.Return().Text("Hello, ").Op("+").ID("name").Semicolon(), + ), + ) + + var source bytes.Buffer + if err := ts.Render(&source, file); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // export default function Greeting(name: string) { + // return "Hello, " + name; + // } +} + +func ExampleJSXElement() { + component := ts.ExportDefault( + ts.Function().ID("Greeting").Bracket( + ts.ID("name").Colon().String(), + ).Block( + ts.Return().Join(ts.JSXElement( + ts.ID("div"), + []types.Token{ts.JSXAttribute("className", "greeting")}, + ts.JSXText("Hello, "), + ts.JSXExpr(ts.ID("name")), + )), + ), + ) + + var source bytes.Buffer + if err := ts.Render(&source, component); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // export default function Greeting(name: string) { + // return
Hello, {name}
+ // } +} + +func ExampleImport() { + userType := ts.Export().Type().ID("User").Op("=").Block( + ts.ID("id").Colon().String().Semicolon(), + ts.ID("name").Colon().String().Semicolon(), + ).Semicolon() + file := ts.Import("node:fs/promises", ts.ImportNames(ts.ID("readFile"))).Line().Join(userType) + + var source bytes.Buffer + if err := ts.Render(&source, file); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // import { readFile } from "node:fs/promises"; + // export type User = { + // id: string; + // name: string; + // }; +} + +func ExampleJSXAttribute() { + node := ts.JSXElement( + ts.ID("button"), + []types.Token{ + ts.JSXAttribute("type", "button"), + ts.JSXAttributeExpr("disabled", ts.ID("isDisabled")), + ts.JSXSpreadAttribute(ts.ID("props")), + ts.JSXBooleanAttribute("autoFocus"), + }, + ts.JSXText("Save "), + ts.JSXExpr(ts.ID("label")), + ) + + var source bytes.Buffer + if err := ts.Render(&source, node); err != nil { + fmt.Println(err) + return + } + fmt.Print(source.String()) + + // Output: + // +} diff --git a/typescript/import.go b/typescript/import.go new file mode 100644 index 0000000..e88b0cb --- /dev/null +++ b/typescript/import.go @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "fmt" + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +type importDeclaration struct { + module string + clause []types.Token + typeOnly bool +} + +func (v *importDeclaration) Render(w io.Writer) error { + parts := []types.Token{keyword("import")} + if v.typeOnly { + parts = append(parts, keyword("type")) + } + if len(v.clause) > 0 { + parts = append(parts, v.clause[0]) + for _, clause := range v.clause[1:] { + parts = append(parts, symbol(","), clause) + } + parts = append(parts, keyword("from")) + } + parts = append(parts, text(v.module), symbol(";")) + return gen.Render(w, parts) +} + +func (v *importDeclaration) RenderLayout() gen.Layout { + last := gen.Style{Kind: gen.KindSemicolon, Text: ";"} + return gen.Layout{First: wordStyle("import"), Last: last} +} + +type importNames struct{ specifiers []types.Token } + +func (v *importNames) Render(w io.Writer) error { + if _, err := io.WriteString(w, "{"); err != nil { + return err + } + if len(v.specifiers) > 0 { + if _, err := io.WriteString(w, " "); err != nil { + return err + } + for index, specifier := range v.specifiers { + if index > 0 { + if _, err := io.WriteString(w, ", "); err != nil { + return err + } + } + if err := specifier.Render(w); err != nil { + return err + } + } + if _, err := io.WriteString(w, " "); err != nil { + return err + } + } + _, err := io.WriteString(w, "}") + return err +} + +func (v *importNames) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindBlockOpen, Text: "{"}, + Last: gen.Style{Kind: gen.KindBlockClose, Text: "}"}, + } +} + +type exportDeclaration struct { + declaration types.Token + defaultExport bool +} + +func (v *exportDeclaration) Render(w io.Writer) error { + if v.declaration == nil { + return fmt.Errorf("export declaration is nil") + } + parts := []types.Token{keyword("export")} + if v.defaultExport { + parts = append(parts, keyword("default")) + } + parts = append(parts, v.declaration) + return gen.Render(w, parts) +} + +func (v *exportDeclaration) RenderLayout() gen.Layout { + layout := gen.LayoutOf([]types.Token{v.declaration}) + return gen.Layout{First: wordStyle("export"), Last: layout.Last} +} + +type exportNames struct { + specifiers []types.Token + module string +} + +func (v *exportNames) Render(w io.Writer) error { + parts := []types.Token{keyword("export"), &importNames{specifiers: v.specifiers}} + if v.module != "" { + parts = append(parts, keyword("from"), text(v.module)) + } + parts = append(parts, symbol(";")) + return gen.Render(w, parts) +} + +func (v *exportNames) RenderLayout() gen.Layout { + return gen.Layout{First: wordStyle("export"), Last: gen.Style{Kind: gen.KindSemicolon, Text: ";"}} +} diff --git a/typescript/render_test.go b/typescript/render_test.go new file mode 100644 index 0000000..eefadf5 --- /dev/null +++ b/typescript/render_test.go @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript_test + +import ( + "bytes" + "io" + "testing" + + "go.osspkg.com/gogen/types" + ts "go.osspkg.com/gogen/typescript" +) + +func render(t *testing.T, token interface{ Render(io.Writer) error }) string { + t.Helper() + var out bytes.Buffer + if err := token.Render(&out); err != nil { + t.Fatal(err) + } + return out.String() +} + +func TestTokensRenderExpressions(t *testing.T) { + tests := []struct { + name string + token interface{ Render(io.Writer) error } + want string + }{ + {name: "call and property selector", token: ts.ID("client").Selector("send").Call(ts.Text("hello")), want: `client.send("hello")`}, + {name: "optional chain and nullish coalescing", token: ts.ID("user").OptionalChain("name").Op("??").Text("anonymous"), want: `user?.name ?? "anonymous"`}, + {name: "generic call with array index", token: ts.ID("lookup").TypeArgs(ts.String()).Call(ts.ID("items").Index(ts.Raw("0"))), want: `lookup(items[0])`}, + {name: "array literal", token: ts.ArrayLiteral(ts.Text("a"), ts.Text("b")), want: `["a", "b"]`}, + {name: "array type", token: ts.ArrayType(ts.String()), want: `string[]`}, + {name: "object literal", token: ts.ObjectLiteral(ts.KeyValue(ts.ID("name"), ts.Text("Ada"))), want: "{\n\tname: \"Ada\"\n}"}, + {name: "optional property", token: ts.ID("name").Optional().Colon().String(), want: "name?: string"}, + {name: "non-null assertion", token: ts.ID("value").NonNull(), want: "value!"}, + {name: "template interpolation", token: ts.Template(ts.TemplateText("hello "), ts.TemplateExpr(ts.ID("name"))), want: "`hello ${name}`"}, + {name: "raw is preserved", token: ts.Raw("first\n second"), want: "first\n second"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := render(t, test.token); got != test.want { + t.Fatalf("render() = %q, want %q", got, test.want) + } + }) + } + + got := render(t, ts.Template(ts.TemplateText("` ${value} \\"))) + want := string([]byte{'`', '\\', '`', ' ', '\\', '$', '{', 'v', 'a', 'l', 'u', 'e', '}', ' ', '\\', '\\', '`'}) + if got != want { + t.Fatalf("escaped template = %q, want %q", got, want) + } +} + +func TestTokensRenderDeclarationsAndControlFlow(t *testing.T) { + function := ts.Export().Function().ID("greet"). + Bracket(ts.ID("name").Colon().String()).Colon().String(). + Block( + ts.If().Bracket(ts.ID("name").Op("===").Text("")).Block( + ts.Return().Text("anonymous").Semicolon(), + ).Else().Block( + ts.Return().Text("hello ").Op("+").ID("name").Semicolon(), + ), + ) + file := ts.Import("react", ts.ID("React")).Line().Join(function) + + want := "import React from \"react\";\nexport function greet(name: string): string {\n\tif (name === \"\") {\n\t\treturn \"anonymous\";\n\t} else {\n\t\treturn \"hello \" + name;\n\t}\n}" + if got := render(t, file); got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestTypesAndClassDeclarations(t *testing.T) { + typeAlias := ts.Export().Type().ID("Identifier").Op("=").Union(ts.String(), ts.Number()).Semicolon() + interfaceDecl := ts.Interface().ID("Repository").TypeArgs(ts.ID("T")).Block( + ts.ID("get").Bracket(ts.ID("id").Colon().String()).Colon().ID("T").Semicolon(), + ) + classDecl := ts.Export().Class().ID("UserRepository").Implements().ID("Repository").TypeArgs(ts.ID("User")).Block( + ts.Private().ID("items").Colon().ArrayType(ts.ID("User")).Semicolon(), + ts.Constructor().Bracket(ts.ID("items").Colon().ArrayType(ts.ID("User"))).Block( + ts.This().Selector("items").Op("=").ID("items").Semicolon(), + ), + ) + want := "export type Identifier = string | number;\ninterface Repository {\n\tget(id: string): T;\n}\nexport class UserRepository implements Repository {\n\tprivate items: User[];\n\tconstructor(items: User[]) {\n\t\tthis.items = items;\n\t}\n}" + got := render(t, typeAlias.Line().Join(interfaceDecl, ts.Line(), classDecl)) + if got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestEmptyAndNestedBlocks(t *testing.T) { + empty := render(t, ts.Block()) + if empty != "{}" { + t.Fatalf("empty block = %q, want %q", empty, "{}") + } + + nested := ts.Function().ID("outer").Bracket().Block( + ts.If().Bracket(ts.True()).Block(), + ts.If().Bracket(ts.False()).Block(ts.ID("nested").Call()), + ) + want := "function outer() {\n\tif (true) {}\n\tif (false) {\n\t\tnested()\n\t}\n}" + if got := render(t, nested); got != want { + t.Fatalf("nested blocks =\n%s\nwant:\n%s", got, want) + } +} + +func TestControlFlowAndComments(t *testing.T) { + token := ts.ForOf("item", ts.ID("items")).Block( + ts.Comment("skip non-positive values"), + ts.If().Bracket(ts.ID("item").Op("<=").Raw("0")).Block( + ts.Continue().Semicolon(), + ), + ) + want := "for (const item of items) {\n\t// skip non-positive values\n\tif (item <= 0) {\n\t\tcontinue;\n\t}\n}" + if got := render(t, token); got != want { + t.Fatalf("render() =\n%s\nwant:\n%s", got, want) + } +} + +func TestIdentifierAndOperatorValidation(t *testing.T) { + if got := render(t, ts.ID("变量").Op("+").ID("value")); got != "变量 + value" { + t.Fatalf("Unicode identifier render = %q", got) + } + + for _, token := range []interface{ Render(io.Writer) error }{ + ts.ID("not-an-identifier"), + ts.ID("value").Op("???"), + } { + var out bytes.Buffer + if err := token.Render(&out); err == nil { + t.Errorf("Render(%T) succeeded, want validation error", token) + } + } +} + +func TestStringLiteralEscaping(t *testing.T) { + token := ts.Text("quote: \"; bell: \a; line: \u2028") + want := `"quote: \"; bell: \x07; line: \u2028"` + if got := render(t, token); got != want { + t.Fatalf("string literal = %q, want %q", got, want) + } +} + +func TestImportAndExportBuilders(t *testing.T) { + tests := []struct { + name string + token interface{ Render(io.Writer) error } + want string + }{ + {name: "named import", token: ts.Import("react", ts.ImportNames(ts.ID("useState"), ts.ID("useEffect").As().ID("effect"))), want: `import { useState, useEffect as effect } from "react";`}, + {name: "combined default and named import", token: ts.Import("react", ts.ID("React"), ts.ImportNames(ts.ID("useState"))), want: `import React, { useState } from "react";`}, + {name: "namespace import", token: ts.ImportNamespace("fs", "node:fs"), want: `import * as fs from "node:fs";`}, + {name: "type-only import", token: ts.ImportType("./types", ts.ImportNames(ts.ID("User"))), want: `import type { User } from "./types";`}, + {name: "side-effect import", token: ts.Import("./setup"), want: `import "./setup";`}, + {name: "export names", token: ts.ExportNames(ts.ID("User"), ts.ID("Name").As().ID("UserName")), want: `export { User, Name as UserName };`}, + {name: "re-export names", token: ts.ExportFrom("./model", ts.ID("User")), want: `export { User } from "./model";`}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := render(t, test.token); got != test.want { + t.Fatalf("render() = %q, want %q", got, test.want) + } + }) + } +} + +func TestRenderMatchesTokensRender(t *testing.T) { + token := ts.Const().ID("count").Op("=").Raw("2").Semicolon() + want := render(t, token) + var got bytes.Buffer + if err := ts.Render(&got, token); err != nil { + t.Fatal(err) + } + if got.String() != want { + t.Fatalf("Render() = %q, Tokens.Render() = %q", got.String(), want) + } +} + +func TestTSXRenderingAndEscaping(t *testing.T) { + element := ts.JSXElement( + ts.ID("main"), + []types.Token{ + ts.JSXAttribute("title", `say "hi" & <`), + ts.JSXAttributeExpr("count", ts.ID("count")), + ts.JSXSpreadAttribute(ts.ID("props")), + ts.JSXBooleanAttribute("hidden"), + }, + ts.JSXText("A < B & {value} "), + ts.JSXExpr(ts.ID("name")), + ts.JSXElement(ts.ID("img"), []types.Token{ts.JSXAttribute("alt", "avatar")}), + ) + want := `
A < B & {value} {name}avatar
` + if got := render(t, element); got != want { + t.Fatalf("JSX render() = %q, want %q", got, want) + } + + fragment := ts.JSXFragment(ts.JSXElement(ts.ID("span"), nil, ts.JSXText("ok"))) + if got, want := render(t, fragment), "<>ok"; got != want { + t.Fatalf("fragment render() = %q, want %q", got, want) + } +} + +func TestTSXRejectsInvalidAttributesAndMissingExpressions(t *testing.T) { + tests := []struct { + name string + token interface{ Render(io.Writer) error } + }{ + {name: "invalid attribute name", token: ts.JSXAttribute(`title" onclick="x`, "value")}, + {name: "nil expression", token: ts.JSXExpr(nil)}, + {name: "nil spread expression", token: ts.JSXSpreadAttribute(nil)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var out bytes.Buffer + if err := test.token.Render(&out); err == nil { + t.Fatal("Render() succeeded, want error") + } + }) + } +} diff --git a/typescript/token_control.go b/typescript/token_control.go new file mode 100644 index 0000000..5ab6367 --- /dev/null +++ b/typescript/token_control.go @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import "go.osspkg.com/gogen/types" + +// If starts an if statement. +func (v *Tokens) If() *Tokens { return v.Join(rawWord("if ")) } + +// If starts an if statement. +func If() *Tokens { return create().If() } + +// Else appends an else branch. +func (v *Tokens) Else() *Tokens { return v.Join(keyword("else")) } + +// Else appends an else branch. +func Else() *Tokens { return create().Else() } + +// ElseIf starts an else-if branch. +func (v *Tokens) ElseIf() *Tokens { return v.Join(rawWord("else if ")) } + +// ElseIf starts an else-if branch. +func ElseIf() *Tokens { return create().ElseIf() } + +// For starts a for statement. +func (v *Tokens) For() *Tokens { return v.Join(rawWord("for ")) } + +// For starts a for statement. +func For() *Tokens { return create().For() } + +// ForOf creates a for-of loop with a const binding. +func (v *Tokens) ForOf(name string, iterable types.Token) *Tokens { + return v.Join(rawWord("for (const"), identifier(name), keyword("of"), iterable, symbol(")")) +} + +// ForOf creates a for-of loop with a const binding. +func ForOf(name string, iterable types.Token) *Tokens { return create().ForOf(name, iterable) } + +// ForIn creates a for-in loop with a const binding. +func (v *Tokens) ForIn(name string, object types.Token) *Tokens { + return v.Join(rawWord("for (const"), identifier(name), keyword("in"), object, symbol(")")) +} + +// ForIn creates a for-in loop with a const binding. +func ForIn(name string, object types.Token) *Tokens { return create().ForIn(name, object) } + +// While starts a while loop. +func (v *Tokens) While() *Tokens { return v.Join(rawWord("while ")) } + +// While starts a while loop. +func While() *Tokens { return create().While() } + +// Switch starts a switch statement. +func (v *Tokens) Switch() *Tokens { return v.Join(rawWord("switch ")) } + +// Switch starts a switch statement. +func Switch() *Tokens { return create().Switch() } + +// Case starts a case clause. +func (v *Tokens) Case() *Tokens { return v.Join(keyword("case")) } + +// Case starts a case clause. +func Case() *Tokens { return create().Case() } + +// Default starts a default clause. +func (v *Tokens) Default() *Tokens { return v.Join(keyword("default")) } + +// Default starts a default clause. +func Default() *Tokens { return create().Default() } + +// Break appends a break statement keyword. +func (v *Tokens) Break() *Tokens { return v.Join(keyword("break")) } + +// Break appends a break statement keyword. +func Break() *Tokens { return create().Break() } + +// Continue appends a continue statement keyword. +func (v *Tokens) Continue() *Tokens { return v.Join(keyword("continue")) } + +// Continue appends a continue statement keyword. +func Continue() *Tokens { return create().Continue() } + +// Try starts a try statement. +func (v *Tokens) Try() *Tokens { return v.Join(keyword("try")) } + +// Try starts a try statement. +func Try() *Tokens { return create().Try() } + +// Catch starts a catch clause. +func (v *Tokens) Catch() *Tokens { return v.Join(rawWord("catch ")) } + +// Catch starts a catch clause. +func Catch() *Tokens { return create().Catch() } + +// Finally starts a finally clause. +func (v *Tokens) Finally() *Tokens { return v.Join(keyword("finally")) } + +// Finally starts a finally clause. +func Finally() *Tokens { return create().Finally() } + +// Do starts a do-while loop. +func (v *Tokens) Do() *Tokens { return v.Join(keyword("do")) } + +// Do starts a do-while loop. +func Do() *Tokens { return create().Do() } + +// Semicolon appends a statement terminator. +func (v *Tokens) Semicolon() *Tokens { return v.Join(operation(";")) } + +// Semicolon appends a statement terminator. +func Semicolon() *Tokens { return create().Semicolon() } + +// Comma appends a comma separator. +func (v *Tokens) Comma() *Tokens { return v.Join(operation(",")) } + +// Comma appends a comma separator. +func Comma() *Tokens { return create().Comma() } + +// Colon appends a colon separator. +func (v *Tokens) Colon() *Tokens { return v.Join(operation(":")) } + +// Colon appends a colon separator. +func Colon() *Tokens { return create().Colon() } + +// Optional appends an optional-property marker. +func (v *Tokens) Optional() *Tokens { return v.Join(postfix("?")) } + +// Optional appends an optional-property marker. +func Optional() *Tokens { return create().Optional() } + +// NonNull appends a non-null assertion marker. +func (v *Tokens) NonNull() *Tokens { return v.Join(postfix("!")) } + +// NonNull appends a non-null assertion marker. +func NonNull() *Tokens { return create().NonNull() } + +// Spread appends the spread operator. +func (v *Tokens) Spread() *Tokens { return v.Join(operation("...")) } + +// Spread appends the spread operator. +func Spread() *Tokens { return create().Spread() } + +// Arrow appends the arrow-function operator. +func (v *Tokens) Arrow() *Tokens { return v.Join(operation("=>")) } + +// Arrow appends the arrow-function operator. +func Arrow() *Tokens { return create().Arrow() } + +// As appends the as keyword for aliases or type assertions. +func (v *Tokens) As() *Tokens { return v.Join(keyword("as")) } + +// As appends the as keyword for aliases or type assertions. +func As() *Tokens { return create().As() } + +// Of appends the of keyword. +func (v *Tokens) Of() *Tokens { return v.Join(keyword("of")) } + +// Of appends the of keyword. +func Of() *Tokens { return create().Of() } diff --git a/typescript/token_declarations.go b/typescript/token_declarations.go new file mode 100644 index 0000000..92f1c3e --- /dev/null +++ b/typescript/token_declarations.go @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import "go.osspkg.com/gogen/types" + +// Import creates an import declaration from a module. +func (v *Tokens) Import(module string, clause ...types.Token) *Tokens { + return v.Join(&importDeclaration{module: module, clause: clause}) +} + +// Import creates an import declaration from a module. +func Import(module string, clause ...types.Token) *Tokens { return create().Import(module, clause...) } + +// ImportType creates a type-only import declaration. +func (v *Tokens) ImportType(module string, clause ...types.Token) *Tokens { + return v.Join(&importDeclaration{module: module, clause: clause, typeOnly: true}) +} + +// ImportType creates a type-only import declaration. +func ImportType(module string, clause ...types.Token) *Tokens { + return create().ImportType(module, clause...) +} + +// ImportNames creates a named import or export specifier list. +func (v *Tokens) ImportNames(specifiers ...types.Token) *Tokens { + return v.Join(&importNames{specifiers: specifiers}) +} + +// ImportNames creates a named import or export specifier list. +func ImportNames(specifiers ...types.Token) *Tokens { return create().ImportNames(specifiers...) } + +// ImportNamespace creates a namespace import declaration. +func (v *Tokens) ImportNamespace(alias, module string) *Tokens { + return v.Import(module, rawWord("* as").Join(identifier(alias))) +} + +// ImportNamespace creates a namespace import declaration. +func ImportNamespace(alias, module string) *Tokens { return create().ImportNamespace(alias, module) } + +// Export appends the export keyword. +func (v *Tokens) Export() *Tokens { return v.Join(keyword("export")) } + +// Export appends the export keyword. +func Export() *Tokens { return create().Export() } + +// ExportDefault creates a default export declaration. +func (v *Tokens) ExportDefault(declaration types.Token) *Tokens { + return v.Join(&exportDeclaration{declaration: declaration, defaultExport: true}) +} + +// ExportDefault creates a default export declaration. +func ExportDefault(declaration types.Token) *Tokens { return create().ExportDefault(declaration) } + +// ExportNames creates a named export declaration. +func (v *Tokens) ExportNames(specifiers ...types.Token) *Tokens { + return v.Join(&exportNames{specifiers: specifiers}) +} + +// ExportNames creates a named export declaration. +func ExportNames(specifiers ...types.Token) *Tokens { return create().ExportNames(specifiers...) } + +// ExportFrom creates a re-export declaration from a module. +func (v *Tokens) ExportFrom(module string, specifiers ...types.Token) *Tokens { + return v.Join(&exportNames{specifiers: specifiers, module: module}) +} + +// ExportFrom creates a re-export declaration from a module. +func ExportFrom(module string, specifiers ...types.Token) *Tokens { + return create().ExportFrom(module, specifiers...) +} + +// From appends a from clause with a quoted module path. +func (v *Tokens) From(module string) *Tokens { return v.Join(keyword("from"), text(module)) } + +// Type appends the type-alias declaration keyword. +func (v *Tokens) Type() *Tokens { return v.Join(keyword("type")) } + +// Type appends the type-alias declaration keyword. +func Type() *Tokens { return create().Type() } + +// Interface appends the interface declaration keyword. +func (v *Tokens) Interface() *Tokens { return v.Join(keyword("interface")) } + +// Interface appends the interface declaration keyword. +func Interface() *Tokens { return create().Interface() } + +// Class appends the class declaration keyword. +func (v *Tokens) Class() *Tokens { return v.Join(keyword("class")) } + +// Class appends the class declaration keyword. +func Class() *Tokens { return create().Class() } + +// Function appends the function declaration keyword. +func (v *Tokens) Function() *Tokens { return v.Join(keyword("function")) } + +// Function appends the function declaration keyword. +func Function() *Tokens { return create().Function() } + +// Const appends the const declaration keyword. +func (v *Tokens) Const() *Tokens { return v.Join(keyword("const")) } + +// Const appends the const declaration keyword. +func Const() *Tokens { return create().Const() } + +// Let appends the let declaration keyword. +func (v *Tokens) Let() *Tokens { return v.Join(keyword("let")) } + +// Let appends the let declaration keyword. +func Let() *Tokens { return create().Let() } + +// Var appends the var declaration keyword. +func (v *Tokens) Var() *Tokens { return v.Join(keyword("var")) } + +// Var appends the var declaration keyword. +func Var() *Tokens { return create().Var() } + +// Async appends the async modifier. +func (v *Tokens) Async() *Tokens { return v.Join(keyword("async")) } + +// Async appends the async modifier. +func Async() *Tokens { return create().Async() } + +// Await appends the await keyword. +func (v *Tokens) Await() *Tokens { return v.Join(keyword("await")) } + +// Await appends the await keyword. +func Await() *Tokens { return create().Await() } + +// Return appends the return statement keyword. +func (v *Tokens) Return() *Tokens { return v.Join(keyword("return")) } + +// Return appends the return statement keyword. +func Return() *Tokens { return create().Return() } + +// Throw appends the throw statement keyword. +func (v *Tokens) Throw() *Tokens { return v.Join(keyword("throw")) } + +// Throw appends the throw statement keyword. +func Throw() *Tokens { return create().Throw() } + +// Extends appends the extends keyword. +func (v *Tokens) Extends() *Tokens { return v.Join(keyword("extends")) } + +// Extends appends the extends keyword. +func Extends() *Tokens { return create().Extends() } + +// Implements appends the implements keyword. +func (v *Tokens) Implements() *Tokens { return v.Join(keyword("implements")) } + +// Implements appends the implements keyword. +func Implements() *Tokens { return create().Implements() } + +// New appends the new keyword. +func (v *Tokens) New() *Tokens { return v.Join(keyword("new")) } + +// New appends the new keyword. +func New() *Tokens { return create().New() } + +// This appends the this keyword. +func (v *Tokens) This() *Tokens { return v.Join(keyword("this")) } + +// This appends the this keyword. +func This() *Tokens { return create().This() } + +// Namespace appends the namespace declaration keyword. +func (v *Tokens) Namespace() *Tokens { return v.Join(keyword("namespace")) } + +// Namespace appends the namespace declaration keyword. +func Namespace() *Tokens { return create().Namespace() } diff --git a/typescript/token_expressions.go b/typescript/token_expressions.go new file mode 100644 index 0000000..e6c22a5 --- /dev/null +++ b/typescript/token_expressions.go @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// ID appends a validated TypeScript identifier. +func (v *Tokens) ID(name string) *Tokens { return v.Join(identifier(name)) } + +// ID appends a validated TypeScript identifier. +func ID(name string) *Tokens { return create().ID(name) } + +// Raw appends source text verbatim. +func (v *Tokens) Raw(source string) *Tokens { return v.Join(raw(source)) } + +// Raw appends source text verbatim. +func Raw(source string) *Tokens { return create().Raw(source) } + +// Text appends a quoted and escaped JavaScript string literal. +func (v *Tokens) Text(value string) *Tokens { return v.Join(text(value)) } + +// Text appends a quoted and escaped JavaScript string literal. +func Text(value string) *Tokens { return create().Text(value) } + +// Op appends a supported TypeScript operator. +func (v *Tokens) Op(op string) *Tokens { return v.Join(operation(op)) } + +// Op appends a supported TypeScript operator. +func Op(op string) *Tokens { return create().Op(op) } + +// Call creates or appends a parenthesized call argument list. +func (v *Tokens) Call(args ...types.Token) *Tokens { return v.Join(brackets(args...)) } + +// Call creates or appends a parenthesized call argument list. +func Call(args ...types.Token) *Tokens { return create().Call(args...) } + +// Bracket creates or appends a parenthesized token list. +func (v *Tokens) Bracket(args ...types.Token) *Tokens { return v.Join(brackets(args...)) } + +// Bracket creates or appends a parenthesized token list. +func Bracket(args ...types.Token) *Tokens { return create().Bracket(args...) } + +// List creates or appends a comma-separated token list without delimiters. +func (v *Tokens) List(args ...types.Token) *Tokens { return v.Join(list(args...)) } + +// List creates or appends a comma-separated token list without delimiters. +func List(args ...types.Token) *Tokens { return create().List(args...) } + +// Index appends an indexed access expression. +func (v *Tokens) Index(index types.Token) *Tokens { return v.Join(square(index)) } + +// Index appends an indexed access expression. +func Index(index types.Token) *Tokens { return create().Index(index) } + +// TypeArgs creates or appends generic type arguments. +func (v *Tokens) TypeArgs(args ...types.Token) *Tokens { return v.Join(&angleArgs{args: args}) } + +// TypeArgs creates or appends generic type arguments. +func TypeArgs(args ...types.Token) *Tokens { return create().TypeArgs(args...) } + +// KeyValue creates a key-value member for an object or type literal. +func (v *Tokens) KeyValue(key, value types.Token) *Tokens { return v.Join(keyed(key, value)) } + +// KeyValue creates a key-value member for an object or type literal. +func KeyValue(key, value types.Token) *Tokens { return create().KeyValue(key, value) } + +// Block creates or appends a brace-delimited block with indentation. +func (v *Tokens) Block(args ...types.Token) *Tokens { return v.Join(block(args...)) } + +// Block creates or appends a brace-delimited block with indentation. +func Block(args ...types.Token) *Tokens { return create().Block(args...) } + +// Comment appends a TypeScript line comment. +func (v *Tokens) Comment(value string) *Tokens { return v.Join(comment(value)) } + +// Comment appends a TypeScript line comment. +func Comment(value string) *Tokens { return create().Comment(value) } + +// Line appends an explicit line break. +func (v *Tokens) Line() *Tokens { return v.Join(lineToken{}) } + +// Line appends an explicit line break. +func Line() *Tokens { return create().Line() } + +// Pkg appends a module-style dotted prefix. +func (v *Tokens) Pkg(module string) *Tokens { + if module == "" { + return v + } + return v.Join(identifier(module), operation(".")) +} + +// Pkg appends a module-style dotted prefix. +func Pkg(module string) *Tokens { return create().Pkg(module) } + +// ObjectLiteral creates or appends a multiline object literal. +func (v *Tokens) ObjectLiteral(properties ...types.Token) *Tokens { + return v.Join(&objectLiteral{properties: properties}) +} + +// ObjectLiteral creates or appends a multiline object literal. +func ObjectLiteral(properties ...types.Token) *Tokens { return create().ObjectLiteral(properties...) } + +// ArrayLiteral creates or appends an array literal. +func (v *Tokens) ArrayLiteral(values ...types.Token) *Tokens { + return v.Join(square(values...)) +} + +// ArrayLiteral creates or appends an array literal. +func ArrayLiteral(values ...types.Token) *Tokens { return create().ArrayLiteral(values...) } + +// Template creates or appends a template string from text and expressions. +func (v *Tokens) Template(parts ...types.Token) *Tokens { + return v.Join(&templateLiteral{parts: parts}) +} + +// Template creates or appends a template string from text and expressions. +func Template(parts ...types.Token) *Tokens { return create().Template(parts...) } + +// TemplateText creates a token containing escaped template-string text. +func TemplateText(value string) types.Token { return &templateText{value: value} } + +// TemplateExpr creates an interpolated template-string expression token. +func TemplateExpr(expression types.Token) types.Token { + return &templateExpression{expression: expression} +} + +// NewCall creates a constructor call expression. +func (v *Tokens) NewCall(target types.Token, args ...types.Token) *Tokens { + return v.Join(keyword("new"), target, brackets(args...)) +} + +// NewCall creates a constructor call expression. +func NewCall(target types.Token, args ...types.Token) *Tokens { + return create().NewCall(target, args...) +} + +// Selector appends a property selector. +func (v *Tokens) Selector(name string) *Tokens { return v.Join(operation("."), identifier(name)) } + +// Selector appends a property selector. +func Selector(name string) *Tokens { return create().Selector(name) } + +// OptionalChain appends an optional property selector. +func (v *Tokens) OptionalChain(name string) *Tokens { return v.Join(operation("?."), identifier(name)) } + +// OptionalChain appends an optional property selector. +func OptionalChain(name string) *Tokens { return create().OptionalChain(name) } + +// ArrayType creates an array type from its element type. +func (v *Tokens) ArrayType(element types.Token) *Tokens { + return v.Join(element, &models.SquareBracket{}) +} + +// ArrayType creates an array type from its element type. +func ArrayType(element types.Token) *Tokens { return create().ArrayType(element) } diff --git a/typescript/token_modifiers.go b/typescript/token_modifiers.go new file mode 100644 index 0000000..45abbee --- /dev/null +++ b/typescript/token_modifiers.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +// Public appends the public access modifier. +func (v *Tokens) Public() *Tokens { return v.Join(keyword("public")) } + +// Public appends the public access modifier. +func Public() *Tokens { return create().Public() } + +// Private appends the private access modifier. +func (v *Tokens) Private() *Tokens { return v.Join(keyword("private")) } + +// Private appends the private access modifier. +func Private() *Tokens { return create().Private() } + +// Protected appends the protected access modifier. +func (v *Tokens) Protected() *Tokens { return v.Join(keyword("protected")) } + +// Protected appends the protected access modifier. +func Protected() *Tokens { return create().Protected() } + +// Static appends the static modifier. +func (v *Tokens) Static() *Tokens { return v.Join(keyword("static")) } + +// Static appends the static modifier. +func Static() *Tokens { return create().Static() } + +// Readonly appends the readonly modifier. +func (v *Tokens) Readonly() *Tokens { return v.Join(keyword("readonly")) } + +// Readonly appends the readonly modifier. +func Readonly() *Tokens { return create().Readonly() } + +// Abstract appends the abstract modifier. +func (v *Tokens) Abstract() *Tokens { return v.Join(keyword("abstract")) } + +// Abstract appends the abstract modifier. +func Abstract() *Tokens { return create().Abstract() } + +// Override appends the override modifier. +func (v *Tokens) Override() *Tokens { return v.Join(keyword("override")) } + +// Override appends the override modifier. +func Override() *Tokens { return create().Override() } + +// Constructor appends the constructor keyword. +func (v *Tokens) Constructor() *Tokens { return v.Join(keyword("constructor")) } + +// Constructor appends the constructor keyword. +func Constructor() *Tokens { return create().Constructor() } diff --git a/typescript/token_tsx.go b/typescript/token_tsx.go new file mode 100644 index 0000000..6adbfd8 --- /dev/null +++ b/typescript/token_tsx.go @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/types" +) + +var jsxAttributeName = regexp.MustCompile(`^[A-Za-z_:][A-Za-z0-9_.:-]*$`) + +// JSXElement creates a structural TSX element with attributes and children. +func (v *Tokens) JSXElement(tag types.Token, attributes []types.Token, children ...types.Token) *Tokens { + return v.Join(&jsxElement{tag: tag, attributes: attributes, children: children}) +} + +// JSXElement creates a structural TSX element with attributes and children. +func JSXElement(tag types.Token, attributes []types.Token, children ...types.Token) *Tokens { + return create().JSXElement(tag, attributes, children...) +} + +// JSXFragment creates a TSX fragment containing child nodes. +func (v *Tokens) JSXFragment(children ...types.Token) *Tokens { + return v.Join(&jsxFragment{children: children}) +} + +// JSXFragment creates a TSX fragment containing child nodes. +func JSXFragment(children ...types.Token) *Tokens { return create().JSXFragment(children...) } + +// JSXAttribute creates a quoted and escaped TSX string attribute. +func (v *Tokens) JSXAttribute(name, value string) *Tokens { + return v.Join(&jsxAttribute{name: name, value: value}) +} + +// JSXAttribute creates a quoted and escaped TSX string attribute. +func JSXAttribute(name, value string) *Tokens { return create().JSXAttribute(name, value) } + +// JSXAttributeExpr creates a TSX attribute containing an expression. +func (v *Tokens) JSXAttributeExpr(name string, expression types.Token) *Tokens { + return v.Join(&jsxAttributeExpression{name: name, expression: expression}) +} + +// JSXAttributeExpr creates a TSX attribute containing an expression. +func JSXAttributeExpr(name string, expression types.Token) *Tokens { + return create().JSXAttributeExpr(name, expression) +} + +// JSXBooleanAttribute creates a valueless TSX boolean attribute. +func (v *Tokens) JSXBooleanAttribute(name string) *Tokens { + return v.Join(&jsxBooleanAttribute{name: name}) +} + +// JSXBooleanAttribute creates a valueless TSX boolean attribute. +func JSXBooleanAttribute(name string) *Tokens { return create().JSXBooleanAttribute(name) } + +// JSXSpreadAttribute creates a TSX spread attribute from an expression. +func (v *Tokens) JSXSpreadAttribute(expression types.Token) *Tokens { + return v.Join(&jsxSpreadAttribute{expression: expression}) +} + +// JSXSpreadAttribute creates a TSX spread attribute from an expression. +func JSXSpreadAttribute(expression types.Token) *Tokens { + return create().JSXSpreadAttribute(expression) +} + +// JSXText creates an escaped TSX text node. +func (v *Tokens) JSXText(value string) *Tokens { return v.Join(&jsxText{value: value}) } + +// JSXText creates an escaped TSX text node. +func JSXText(value string) *Tokens { return create().JSXText(value) } + +// JSXExpr creates a TSX expression child. +func (v *Tokens) JSXExpr(expression types.Token) *Tokens { + return v.Join(&jsxExpression{expression: expression}) +} + +// JSXExpr creates a TSX expression child. +func JSXExpr(expression types.Token) *Tokens { return create().JSXExpr(expression) } + +type jsxElement struct { + tag types.Token + attributes []types.Token + children []types.Token +} + +func (v *jsxElement) Render(w io.Writer) error { + if v.tag == nil { + return errors.New("TSX element has no tag") + } + if _, err := io.WriteString(w, "<"); err != nil { + return err + } + if err := v.tag.Render(w); err != nil { + return err + } + for _, attribute := range v.attributes { + if _, err := io.WriteString(w, " "); err != nil { + return err + } + if err := attribute.Render(w); err != nil { + return err + } + } + if len(v.children) == 0 { + _, err := io.WriteString(w, " />") + return err + } + if _, err := io.WriteString(w, ">"); err != nil { + return err + } + for _, child := range v.children { + if child == nil { + return errors.New("TSX element has a nil child") + } + if err := child.Render(w); err != nil { + return err + } + } + if _, err := io.WriteString(w, "") + return err +} + +func (v *jsxElement) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindFragment} + return gen.Layout{First: style, Last: style} +} + +type jsxFragment struct{ children []types.Token } + +func (v *jsxFragment) Render(w io.Writer) error { + if _, err := io.WriteString(w, "<>"); err != nil { + return err + } + for _, child := range v.children { + if child == nil { + return errors.New("TSX fragment has a nil child") + } + if err := child.Render(w); err != nil { + return err + } + } + _, err := io.WriteString(w, "") + return err +} +func (v *jsxFragment) RenderLayout() gen.Layout { return jsxElementLayout() } + +func jsxElementLayout() gen.Layout { + style := gen.Style{Kind: gen.KindFragment} + return gen.Layout{First: style, Last: style} +} + +type jsxAttribute struct { + name string + value string +} + +func (v *jsxAttribute) Render(w io.Writer) error { + if !jsxAttributeName.MatchString(v.name) { + return fmt.Errorf("invalid TSX attribute name %q", v.name) + } + if _, err := io.WriteString(w, v.name+`="`); err != nil { + return err + } + if _, err := io.WriteString(w, escapeJSXAttribute(v.value)); err != nil { + return err + } + _, err := io.WriteString(w, `"`) + return err +} +func (v *jsxAttribute) RenderLayout() gen.Layout { return jsxElementLayout() } + +type jsxAttributeExpression struct { + name string + expression types.Token +} + +func (v *jsxAttributeExpression) Render(w io.Writer) error { + if !jsxAttributeName.MatchString(v.name) { + return fmt.Errorf("invalid TSX attribute name %q", v.name) + } + if v.expression == nil { + return fmt.Errorf("TSX attribute %q has no expression", v.name) + } + if _, err := io.WriteString(w, v.name+"={"); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} +func (v *jsxAttributeExpression) RenderLayout() gen.Layout { return jsxElementLayout() } + +type jsxBooleanAttribute struct{ name string } + +func (v *jsxBooleanAttribute) Render(w io.Writer) error { + if !jsxAttributeName.MatchString(v.name) { + return fmt.Errorf("invalid TSX attribute name %q", v.name) + } + _, err := io.WriteString(w, v.name) + return err +} +func (v *jsxBooleanAttribute) RenderLayout() gen.Layout { return jsxElementLayout() } + +type jsxSpreadAttribute struct{ expression types.Token } + +func (v *jsxSpreadAttribute) Render(w io.Writer) error { + if v.expression == nil { + return errors.New("TSX spread attribute has no expression") + } + if _, err := io.WriteString(w, "{..."); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} +func (v *jsxSpreadAttribute) RenderLayout() gen.Layout { return jsxElementLayout() } + +type jsxText struct{ value string } + +func (v *jsxText) Render(w io.Writer) error { + _, err := io.WriteString(w, escapeJSXText(v.value)) + return err +} +func (v *jsxText) RenderLayout() gen.Layout { return jsxElementLayout() } + +type jsxExpression struct{ expression types.Token } + +func (v *jsxExpression) Render(w io.Writer) error { + if v.expression == nil { + return errors.New("TSX expression is nil") + } + if _, err := io.WriteString(w, "{"); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} +func (v *jsxExpression) RenderLayout() gen.Layout { return jsxElementLayout() } + +func escapeJSXAttribute(value string) string { + return strings.NewReplacer("&", "&", `"`, """, "<", "<", ">", ">", "\n", " ", "\r", " ", "\t", " ").Replace(value) +} + +func escapeJSXText(value string) string { + return strings.NewReplacer("&", "&", "<", "<", ">", ">", "{", "{", "}", "}").Replace(value) +} diff --git a/typescript/token_types.go b/typescript/token_types.go new file mode 100644 index 0000000..cdfc42e --- /dev/null +++ b/typescript/token_types.go @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "io" + "strings" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// Any appends the any type. +func (v *Tokens) Any() *Tokens { return v.Join(keyword("any")) } + +// Any appends the any type. +func Any() *Tokens { return create().Any() } + +// Unknown appends the unknown type. +func (v *Tokens) Unknown() *Tokens { return v.Join(keyword("unknown")) } + +// Unknown appends the unknown type. +func Unknown() *Tokens { return create().Unknown() } + +// Never appends the never type. +func (v *Tokens) Never() *Tokens { return v.Join(keyword("never")) } + +// Never appends the never type. +func Never() *Tokens { return create().Never() } + +// Void appends the void type. +func (v *Tokens) Void() *Tokens { return v.Join(keyword("void")) } + +// Void appends the void type. +func Void() *Tokens { return create().Void() } + +// String appends the string type. +func (v *Tokens) String() *Tokens { return v.Join(keyword("string")) } + +// String appends the string type. +func String() *Tokens { return create().String() } + +// Number appends the number type. +func (v *Tokens) Number() *Tokens { return v.Join(keyword("number")) } + +// Number appends the number type. +func Number() *Tokens { return create().Number() } + +// Boolean appends the boolean type. +func (v *Tokens) Boolean() *Tokens { return v.Join(keyword("boolean")) } + +// Boolean appends the boolean type. +func Boolean() *Tokens { return create().Boolean() } + +// BigInt appends the bigint type. +func (v *Tokens) BigInt() *Tokens { return v.Join(keyword("bigint")) } + +// BigInt appends the bigint type. +func BigInt() *Tokens { return create().BigInt() } + +// Symbol appends the symbol type. +func (v *Tokens) Symbol() *Tokens { return v.Join(keyword("symbol")) } + +// Symbol appends the symbol type. +func Symbol() *Tokens { return create().Symbol() } + +// ObjectType appends the object type. +func (v *Tokens) ObjectType() *Tokens { return v.Join(keyword("object")) } + +// ObjectType appends the object type. +func ObjectType() *Tokens { return create().ObjectType() } + +// Null appends the null value. +func (v *Tokens) Null() *Tokens { return v.Join(keyword("null")) } + +// Null appends the null value. +func Null() *Tokens { return create().Null() } + +// Undefined appends the undefined value. +func (v *Tokens) Undefined() *Tokens { return v.Join(identifier("undefined")) } + +// Undefined appends the undefined value. +func Undefined() *Tokens { return create().Undefined() } + +// True appends the true literal. +func (v *Tokens) True() *Tokens { return v.Join(keyword("true")) } + +// True appends the true literal. +func True() *Tokens { return create().True() } + +// False appends the false literal. +func (v *Tokens) False() *Tokens { return v.Join(keyword("false")) } + +// False appends the false literal. +func False() *Tokens { return create().False() } + +// RecordType creates a Record type from its key and value types. +func (v *Tokens) RecordType(key, value types.Token) *Tokens { + return v.Join(keyword("Record"), &angleArgs{args: []types.Token{key, value}}) +} + +// RecordType creates a Record type from its key and value types. +func RecordType(key, value types.Token) *Tokens { return create().RecordType(key, value) } + +// MapType creates a Map type from its key and value types. +func (v *Tokens) MapType(key, value types.Token) *Tokens { + return v.Join(keyword("Map"), &angleArgs{args: []types.Token{key, value}}) +} + +// MapType creates a Map type from its key and value types. +func MapType(key, value types.Token) *Tokens { return create().MapType(key, value) } + +// SetType creates a Set type from its element type. +func (v *Tokens) SetType(value types.Token) *Tokens { + return v.Join(keyword("Set"), &angleArgs{args: []types.Token{value}}) +} + +// SetType creates a Set type from its element type. +func SetType(value types.Token) *Tokens { return create().SetType(value) } + +// PromiseType creates a Promise type from its resolved value type. +func (v *Tokens) PromiseType(value types.Token) *Tokens { + return v.Join(keyword("Promise"), &angleArgs{args: []types.Token{value}}) +} + +// PromiseType creates a Promise type from its resolved value type. +func PromiseType(value types.Token) *Tokens { return create().PromiseType(value) } + +// Union creates a union type from its alternatives. +func (v *Tokens) Union(types ...types.Token) *Tokens { + return appendSeparated(v, types, "|") +} + +// Union creates a union type from its alternatives. +func Union(types ...types.Token) *Tokens { return create().Union(types...) } + +// Intersection creates an intersection type from its constituent types. +func (v *Tokens) Intersection(types ...types.Token) *Tokens { + return appendSeparated(v, types, "&") +} + +// Intersection creates an intersection type from its constituent types. +func Intersection(types ...types.Token) *Tokens { return create().Intersection(types...) } + +func appendSeparated(v *Tokens, values []types.Token, separator string) *Tokens { + for index, value := range values { + if index > 0 { + v = v.Op(separator) + } + v = v.Join(value) + } + return v +} + +type objectLiteral struct{ properties []types.Token } + +func (v *objectLiteral) Render(w io.Writer) error { return (&models.Block{D: v.properties}).Render(w) } +func (v *objectLiteral) RenderLayout() gen.Layout { + return gen.LayoutOf([]types.Token{&models.Block{}}) +} + +type templateLiteral struct{ parts []types.Token } + +func (v *templateLiteral) Render(w io.Writer) error { + if _, err := io.WriteString(w, "`"); err != nil { + return err + } + for _, part := range v.parts { + if err := part.Render(w); err != nil { + return err + } + } + _, err := io.WriteString(w, "`") + return err +} + +func (v *templateLiteral) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindLiteral} + return gen.Layout{First: style, Last: style} +} + +type templateText struct{ value string } + +func (v *templateText) Render(w io.Writer) error { + var escaped strings.Builder + for index, r := range v.value { + if r == '\\' || r == '`' || (r == '$' && index+1 < len(v.value) && v.value[index+1] == '{') { + escaped.WriteByte('\\') + } + escaped.WriteRune(r) + } + _, err := io.WriteString(w, escaped.String()) + return err +} +func (v *templateText) RenderLayout() gen.Layout { return gen.Layout{} } + +type templateExpression struct{ expression types.Token } + +func (v *templateExpression) Render(w io.Writer) error { + if _, err := io.WriteString(w, "${"); err != nil { + return err + } + if err := v.expression.Render(w); err != nil { + return err + } + _, err := io.WriteString(w, "}") + return err +} +func (v *templateExpression) RenderLayout() gen.Layout { return gen.Layout{} } + +type angleArgs struct{ args []types.Token } + +func (v *angleArgs) Render(w io.Writer) error { + if _, err := io.WriteString(w, "<"); err != nil { + return err + } + for index, arg := range v.args { + if index > 0 { + if _, err := io.WriteString(w, ", "); err != nil { + return err + } + } + if err := arg.Render(w); err != nil { + return err + } + } + _, err := io.WriteString(w, ">") + return err +} + +func (v *angleArgs) RenderLayout() gen.Layout { + return gen.Layout{ + First: gen.Style{Kind: gen.KindDot, Text: "<"}, + Last: gen.Style{Kind: gen.KindFragment, Text: ">"}, + } +} + +type lineToken struct{} + +func (lineToken) Render(w io.Writer) error { + _, err := io.WriteString(w, "\n") + return err +} +func (lineToken) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindLine, Text: "\n"} + return gen.Layout{First: style, Last: style} +} diff --git a/typescript/tokens.go b/typescript/tokens.go new file mode 100644 index 0000000..116c5cb --- /dev/null +++ b/typescript/tokens.go @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package typescript + +import ( + "io" + + "go.osspkg.com/gogen/internal/gen" + "go.osspkg.com/gogen/internal/models" + "go.osspkg.com/gogen/types" +) + +// Tokens is a chainable sequence of TypeScript or TSX tokens. +type Tokens []types.Token + +func create() *Tokens { return &Tokens{} } + +// Render writes arg to w without running a formatter or compiler. +func Render(w io.Writer, arg types.Token) error { + return arg.Render(w) +} + +// Render writes the token sequence to w without formatting it. +func (v *Tokens) Render(w io.Writer) error { + return gen.Render(w, []types.Token(*v)) +} + +// Unwrap returns the underlying token slice without copying it. +func (v *Tokens) Unwrap() []types.Token { return *v } + +// Join appends tokens to the sequence and returns it. +func (v *Tokens) Join(args ...types.Token) *Tokens { + *v = append(*v, args...) + return v +} + +func wordStyle(value string) gen.Style { + return gen.Style{Kind: gen.KindWord, Text: value, CanEndExpression: (languageConfig{}).CanEndExpression(value)} +} + +func rawWord(value string) *Tokens { + return create().Join(&tokenRaw{value: value}) +} + +type tokenRaw struct{ value string } + +func (v *tokenRaw) Render(w io.Writer) error { + _, err := io.WriteString(w, v.value) + return err +} + +func (v *tokenRaw) RenderLayout() gen.Layout { + style := gen.Style{Kind: gen.KindFragment, Text: v.value, CanEndExpression: (languageConfig{}).CanEndExpression(v.value)} + return gen.Layout{First: style, Last: style} +} + +func symbol(value string) types.Token { + kind := models.LayoutKind((languageConfig{}).OperationKind(value)) + return gen.Symbol(value, gen.Style{Kind: kind, Text: value}) +} + +func postfix(value string) types.Token { + return gen.Symbol(value, gen.Style{Kind: gen.KindPostfixOperator, Text: value}) +} + +func keyword(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value} +} + +func raw(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value, Raw: true} +} + +func identifier(value string) types.Token { + return &models.Keyword[languageConfig]{C: languageConfig{}, D: value, Verify: true} +} + +func text(value string) types.Token { + return &models.Text[languageConfig]{C: languageConfig{}, D: value} +} + +func operation(value string) types.Token { + return &models.Operation[languageConfig]{D: value} +} + +func comment(value string) types.Token { + return &models.Comment[languageConfig]{D: value} +} + +func block(args ...types.Token) types.Token { + return &models.Block{D: args} +} + +func brackets(args ...types.Token) types.Token { + return &models.Bracket{D: args, Brace: true} +} + +func list(args ...types.Token) types.Token { + return &models.Bracket{D: args} +} + +func square(args ...types.Token) types.Token { + return &models.SquareBracket{D: args} +} + +func keyed(key, value types.Token) types.Token { + return &models.KeyValue{Key: key, Value: value} +}