From 2f7b46659d6f9d7e591db1cce778b862f288a0a6 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Sun, 12 Jan 2025 20:50:19 -0600 Subject: [PATCH] Copy over everything from the other repo --- .github/workflows/ci.yml | 33 +++ .gitignore | 4 + .versions/devctl | 1 + .vscode/extensions.json | 5 + Makefile | 49 ++++ README.md | 41 +++ go.mod | 20 ++ go.sum | 32 +++ hack/example.envrc | 4 + make.go | 209 +++++++++++++++ make_suite_test.go | 13 + scan.go | 60 +++++ scan_test.go | 109 ++++++++ scanner.go | 105 ++++++++ scanner_test.go | 120 +++++++++ token/position.go | 10 + token/token.go | 289 ++++++++++++++++++++ token/token_suite_test.go | 13 + token/token_test.go | 545 ++++++++++++++++++++++++++++++++++++++ write.go | 106 ++++++++ write_test.go | 108 ++++++++ 21 files changed, 1876 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .versions/devctl create mode 100644 .vscode/extensions.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/example.envrc create mode 100644 make.go create mode 100644 make_suite_test.go create mode 100644 scan.go create mode 100644 scan_test.go create mode 100644 scanner.go create mode 100644 scanner_test.go create mode 100644 token/position.go create mode 100644 token/token.go create mode 100644 token/token_suite_test.go create mode 100644 token/token_test.go create mode 100644 write.go create mode 100644 write_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..986adfe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + # For code coverage reports + branches: [main] + pull_request: + branches: [main] + +permissions: + id-token: write + contents: read + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - run: make build + - run: make test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + use_oidc: true + files: cover.profile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b95220d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.make/ +bin/ +*.test +.envrc diff --git a/.versions/devctl b/.versions/devctl new file mode 100644 index 0000000..d917d3e --- /dev/null +++ b/.versions/devctl @@ -0,0 +1 @@ +0.1.2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1314c7a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "golang.go" + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c8523b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +_ := $(shell mkdir -p .make bin) + +WORKING_DIR := $(shell pwd) +LOCALBIN := ${WORKING_DIR}/bin + +export GOBIN := ${LOCALBIN} + +DEVCTL := ${LOCALBIN}/devctl +GINKGO := ${LOCALBIN}/ginkgo + +ifeq ($(CI),) +TEST_FLAGS := --label-filter !E2E +else +TEST_FLAGS := --github-output --race --trace --coverprofile=cover.profile +endif + +build: .make/build +test: .make/test +tidy: go.sum + +test_all: + $(GINKGO) run -r ./ + +go.sum: go.mod $(shell $(DEVCTL) list --go) | bin/devctl + go mod tidy + +%_suite_test.go: | bin/ginkgo + cd $(dir $@) && $(GINKGO) bootstrap + +%_test.go: | bin/ginkgo + cd $(dir $@) && $(GINKGO) generate $(notdir $*) + +bin/ginkgo: go.mod + go install github.com/onsi/ginkgo/v2/ginkgo + +bin/devctl: .versions/devctl + go install github.com/unmango/devctl/cmd@v$(shell cat $<) + mv ${LOCALBIN}/cmd $@ + +.envrc: hack/example.envrc + cp $< $@ + +.make/build: $(shell $(DEVCTL) list --go --exclude-tests) | bin/devctl + go build ./... + @touch $@ + +.make/test: $(shell $(DEVCTL) list --go) | bin/ginkgo bin/devctl + $(GINKGO) run ${TEST_FLAGS} $(sort $(dir $?)) + @touch $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f34886 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Go Make + +Makefile parsing and utilities in Go + +## Usage + +At present the scanning utilities are the most tested. + +Using `make.ScanTokens` with a `bufio.Scanner` + +```go +f := os.Open("Makefile") +s := bufio.NewScanner(f) +s.Split(make.ScanTokens) + +for s.Scan() { + s.Bytes() // The current token byte slice i.e. []byte(":=") + s.Text() // The current token as a string i.e. ":=" +} +``` + +Using `make.Scanner` + +```go +f := os.Open("Makefile") +s := make.NewScanner(f) + +for s.Scan() { + s.Token() // The current token.Token i.e. token.SIMPLE_ASSIGN + s.Literal() // Literal tokens as a string i.e. "identifier" +} + +if err := s.Err(); err != nil { + fmt.Println(err) +} +``` + +## Future + +- `make.Parser` +- `make.Parse(file)` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d87ef0b --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/unmango/go-make + +go 1.23.4 + +require ( + github.com/onsi/ginkgo/v2 v2.22.2 + github.com/onsi/gomega v1.36.2 +) + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f9d6da2 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/example.envrc b/hack/example.envrc new file mode 100644 index 0000000..af75002 --- /dev/null +++ b/hack/example.envrc @@ -0,0 +1,4 @@ +#!/bin/bash + +root="$(git rev-parse --show-toplevel)" +export PATH="$root/bin:$PATH" diff --git a/make.go b/make.go new file mode 100644 index 0000000..4c01972 --- /dev/null +++ b/make.go @@ -0,0 +1,209 @@ +package make + +const ( + DefineDirective = "define" + EndefDirective = "endef" + UndefineDirective = "undefine" + IfdefDirective = "ifdef" + IfndefDirective = "ifndef" + IfeqDirective = "ifeq" + IfneqDirective = "ifneq" + ElseDirective = "else" + EndifDirective = "endif" + IncludeDirective = "include" + DashIncludeDirective = "-include" + SincludeDirective = "sinclude" + OverrideDirective = "override" + ExportDirective = "export" + UnexportDirective = "unexport" + PrivateDirective = "private" + VpathDirective = "vpath" +) + +var Directives = []string{ + DefineDirective, + EndefDirective, + UndefineDirective, + IfdefDirective, + IfndefDirective, + IfeqDirective, + IfneqDirective, + ElseDirective, + EndifDirective, + IncludeDirective, + DashIncludeDirective, + SincludeDirective, + OverrideDirective, + ExportDirective, + UndefineDirective, + PrivateDirective, + VpathDirective, +} + +const ( + SubstFunction = "subst" + PatsubstFunction = "patsubst" + StripFunction = "strip" + FindstringFunction = "findstring" + FilterFunction = "filter" + FilterOutFunction = "filter-out" + SortFunction = "sort" + WordFunction = "word" + WordsFunction = "words" + WordlistFunction = "wordlist" + FirstwordFunction = "firstword" + LastwordFunction = "lastword" + DirFunction = "dir" + NotdirFunction = "notdir" + SuffixFunction = "suffix" + BasenameFunction = "basename" + AddsuffixFunction = "addsuffix" + AddprefixFunction = "addprefix" + JoinFunction = "join" + WildcardFunction = "wildcard" + RealpathFunction = "realpath" + AbspathFunction = "abspath" + ErrorFunction = "error" + WarningFunction = "warning" + ShellFunction = "shell" + OriginFunction = "origin" + FlavorFunction = "flavor" + LetFunction = "let" + ForeachFunction = "foreach" + IfFunction = "if" + OrFunction = "or" + AndFunction = "and" + IntcmpFunction = "intcmp" + CallFunction = "call" + EvalFunction = "eval" + FileFunction = "file" + ValueFunction = "value" +) + +var BuiltinFunctions = []string{ + SubstFunction, + PatsubstFunction, + StripFunction, + FindstringFunction, + FilterFunction, + FilterOutFunction, + SortFunction, + WordFunction, + WordsFunction, + WordlistFunction, + FirstwordFunction, + LastwordFunction, + DirFunction, + NotdirFunction, + SuffixFunction, + BasenameFunction, + AddsuffixFunction, + AddprefixFunction, + JoinFunction, + WildcardFunction, + RealpathFunction, + AbspathFunction, + ErrorFunction, + WarningFunction, + ShellFunction, + OriginFunction, + FlavorFunction, + LetFunction, + ForeachFunction, + IfFunction, + OrFunction, + AndFunction, + IntcmpFunction, + CallFunction, + EvalFunction, + FileFunction, + ValueFunction, +} + +const ( + MakefilesVariable = "MAKEFILES" + VpathVariable = "VPATH" + ShellVariable = "SHELL" + MakeshellVariable = "MAKESHELL" + MakeVariable = "MAKE" + MakeVersionVariable = "MAKE_VERSION" + MakeHostVariable = "MAKE_HOST" + MakelevelVariable = "MAKELEVEL" + MakeflagsVariable = "MAKEFLAGS" + GnumakeflagsVariable = "GNUMAKEFLAGS" + MakecmdgoalsVariable = "MAKECMDGOALS" + CurdirVariable = "CURDIR" + SuffixesVariable = "SUFFIXES" + LibpatternsVariable = ".LIBPATTERNS" +) + +var SpecialVariables = []string{ + MakefilesVariable, + VpathVariable, + ShellVariable, + MakeshellVariable, + MakeVariable, + MakeVersionVariable, + MakeHostVariable, + MakelevelVariable, + MakeflagsVariable, + GnumakeflagsVariable, + MakecmdgoalsVariable, + CurdirVariable, + SuffixesVariable, + LibpatternsVariable, +} + +const ( + PhonyTarget = ".PHONY" + SuffixesTarget = ".SUFFIXES" + DefaultTarget = ".DEFAULT" + PreciousTarget = ".PRECIOUS" + IntermediateTarget = ".INTERMEDIATE" + NotintermediateTarget = ".NOTINTERMEDIATE" + SecondaryTarget = ".SECONDARY" + SecondexpansionTarget = ".SECONDEXPANSION" + DeleteOnErrorTarget = ".DELETE_ON_ERROR" + IgnoreTarget = ".IGNORE" + LowResolutionTimeTarget = ".LOW_RESOLUTION_TIME" + SilentTarget = ".SILENT" + ExportAllVariablesTarget = ".EXPORT_ALL_VARIABLES" + NotparallelTarget = ".NOTPARALLEL" + OneshellTarget = ".ONESHELL" + PosixTarget = ".POSIX" +) + +var BuiltinTargets = []string{ + PhonyTarget, + SuffixesTarget, + DefaultTarget, + PreciousTarget, + IntermediateTarget, + NotintermediateTarget, + SecondaryTarget, + SecondexpansionTarget, + DeleteOnErrorTarget, + IgnoreTarget, + LowResolutionTimeTarget, + SilentTarget, + ExportAllVariablesTarget, + NotparallelTarget, + OneshellTarget, + PosixTarget, +} + +type ( + Target string + PreReq string + Recipe string +) + +type Rule struct { + Target []string + PreReqs []string + Recipe []string +} + +type Makefile struct { + Rules []Rule +} diff --git a/make_suite_test.go b/make_suite_test.go new file mode 100644 index 0000000..7cd5e55 --- /dev/null +++ b/make_suite_test.go @@ -0,0 +1,13 @@ +package make_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMake(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Make Suite") +} diff --git a/scan.go b/scan.go new file mode 100644 index 0000000..21985d5 --- /dev/null +++ b/scan.go @@ -0,0 +1,60 @@ +package make + +import ( + "bytes" +) + +// ScanTokens is a [bufio.SplitFunc] for a [bufio.Scanner] that +// scans for tokens supported by the make syntax. +func ScanTokens(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + switch data[0] { + case ' ': + return 1, nil, nil // TODO: Treat this as a token? + case '?': + if len(data) > 1 && data[1] == '=' { + return 2, data[:2], nil + } + case ':': + if len(data) == 1 && !atEOF { + return 0, nil, nil // We need more info to make a decision + } + if bytes.HasPrefix(data, []byte(":::=")) { + return 4, data[:4], nil + } + if bytes.HasPrefix(data, []byte("::=")) { + return 3, data[:3], nil + } + if bytes.HasPrefix(data, []byte(":=")) { + return 2, data[:2], nil + } + + fallthrough + case '#': + if len(data) > 1 && data[1] == ' ' { + return 2, data[:1], nil + } + + fallthrough + case '\n', '\t', '$', '(', ')', '{', '}', ',': + return 1, data[:1], nil + } + + if i := bytes.IndexAny(data, ":\n\t (){},"); i > 0 { + switch data[i] { + case ' ': + return i + 1, data[:i], nil + case ':', '\n', '\t', '(', ')', '{', '}', ',': + return i, data[:i], nil + } + } + + if atEOF { + return len(data), data, nil + } else { + return 0, nil, nil + } +} diff --git a/scan_test.go b/scan_test.go new file mode 100644 index 0000000..73316cb --- /dev/null +++ b/scan_test.go @@ -0,0 +1,109 @@ +package make_test + +import ( + "bufio" + "bytes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/unmango/go-make" +) + +var _ = Describe("Scan", func() { + Describe("ScanTokens", func() { + DescribeTable("Scanner", + Entry("target", + "target:", []string{"target", ":"}, + ), + Entry("target with a separating space", + "target :", []string{"target", ":"}, + ), + Entry("multiple targets", + "target target2:", []string{"target", "target2", ":"}, + ), + Entry("multiple targets with a separating space", + "target target2 :", []string{"target", "target2", ":"}, + ), + Entry("target with a trailing newline", + "target:\n", []string{"target", ":", "\n"}, + ), + Entry("target with a prereq", + "target: prereq", []string{"target", ":", "prereq"}, + ), + Entry("target with a prereq and trailing newline", + "target: prereq\n", []string{"target", ":", "prereq", "\n"}, + ), + Entry("target with multiple prereqs", + "target: prereq prereq2", []string{"target", ":", "prereq", "prereq2"}, + ), + Entry("target with a recipe", + "target:\n\trecipe", []string{"target", ":", "\n", "\t", "recipe"}, + ), + Entry("target with a recipe and trailing newline", + "target:\n\trecipe\n", []string{"target", ":", "\n", "\t", "recipe", "\n"}, + ), + Entry("target with multiple recipes", + "target:\n\trecipe\n\trecipe2", + []string{"target", ":", "\n", "\t", "recipe", "\n", "\t", "recipe2"}, + ), + Entry("comment", + "# comment", []string{"#", "comment"}, + ), + Entry("comment with multiple words", + "# comment word", []string{"#", "comment", "word"}, + ), + Entry("comment with a trailing newline", + "# comment\n", []string{"#", "comment", "\n"}, + ), + Entry("target with a comment", + "target: # comment", []string{"target", ":", "#", "comment"}, + ), + Entry("directive", + "define TEST", []string{"define", "TEST"}, + ), + Entry("prefixed include directive", + "-include foo.mk", []string{"-include", "foo.mk"}, + ), + Entry("variable", + "VAR := test", []string{"VAR", ":=", "test"}, + ), + Entry("variable with a trailing newline", + "VAR := test\n", []string{"VAR", ":=", "test", "\n"}, + ), + Entry("recursive variable", + "VAR = test", []string{"VAR", "=", "test"}, + ), + Entry("posix variable", + "VAR ::= test", []string{"VAR", "::=", "test"}, + ), + Entry("immediate variable", + "VAR :::= test", []string{"VAR", ":::=", "test"}, + ), + Entry("ifndef variable", + "VAR ?= test", []string{"VAR", "?=", "test"}, + ), + Entry("shell variable", + "VAR != test", []string{"VAR", "!=", "test"}, + ), + Entry("info function", + "$(info thing)", []string{"$", "(", "info", "thing", ")"}, + ), + Entry("subst function", + "$(subst from,to,text)", []string{"$", "(", "subst", "from", ",", "to", ",", "text", ")"}, + ), + func(text string, expected []string) { + buf := bytes.NewBufferString(text) + s := bufio.NewScanner(buf) + s.Split(make.ScanTokens) + + tokens := []string{} + for s.Scan() { + tokens = append(tokens, s.Text()) + } + Expect(s.Err()).NotTo(HaveOccurred()) + Expect(tokens).To(Equal(expected)) + }, + ) + }) +}) diff --git a/scanner.go b/scanner.go new file mode 100644 index 0000000..dee6bcd --- /dev/null +++ b/scanner.go @@ -0,0 +1,105 @@ +package make + +import ( + "bufio" + "io" + + "github.com/unmango/go-make/token" +) + +type Scanner struct { + s *bufio.Scanner + + tok token.Token + lit string + + done bool +} + +func NewScanner(r io.Reader) *Scanner { + s := &Scanner{s: bufio.NewScanner(r)} + s.s.Split(ScanTokens) + s.done = !s.s.Scan() + + return s +} + +func (s Scanner) Err() error { + return s.s.Err() +} + +func (s Scanner) Token() token.Token { + return s.tok +} + +func (s Scanner) Literal() string { + return s.lit +} + +func (s *Scanner) Scan() bool { + if s.done { + s.tok = token.EOF + return false + } + + var atNewline bool + + switch txt := s.s.Text(); { + case token.IsIdentifier(txt): + s.lit = txt + if len(txt) > 1 { + s.tok = token.Lookup(txt) + } else { + s.tok = token.IDENT + } + default: + switch txt { + case "=": + s.tok = token.RECURSIVE_ASSIGN + case ":=": + s.tok = token.SIMPLE_ASSIGN + case "::=": + s.tok = token.POSIX_ASSIGN + case ":::=": + s.tok = token.IMMEDIATE_ASSIGN + case "?=": + s.tok = token.IFNDEF_ASSIGN + case "!=": + s.tok = token.SHELL_ASSIGN + case ",": + s.tok = token.COMMA + case "\n": + atNewline = true + s.tok = token.NEWLINE + case "\t": + s.tok = token.TAB + case "(": + s.tok = token.LPAREN + case ")": + s.tok = token.RPAREN + case "{": + s.tok = token.LBRACE + case "}": + s.tok = token.RBRACE + case "$": + s.tok = token.DOLLAR + case ":": + s.tok = token.COLON + case "#": + // TODO + // s.lit = s.scanComment() + s.tok = token.COMMENT + default: + s.tok = token.UNSUPPORTED + s.lit = txt + } + } + + s.done = !s.s.Scan() + if atNewline && s.done { + s.tok = token.EOF + return false + } else { + return true + } +} diff --git a/scanner_test.go b/scanner_test.go new file mode 100644 index 0000000..dd0df20 --- /dev/null +++ b/scanner_test.go @@ -0,0 +1,120 @@ +package make_test + +import ( + "bytes" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/unmango/go-make" + "github.com/unmango/go-make/token" +) + +var _ = Describe("Scanner", func() { + DescribeTable("Scan identifier", + Entry(nil, "ident"), + Entry(nil, "./file/path"), + Entry(nil, "/abs/path"), + Entry(nil, "foo_bar"), + Entry(nil, "foo-bar"), + Entry(nil, "foo123"), + Entry(nil, "123"), + Entry(nil, "_foo"), + Entry(nil, "foo_"), + func(input string) { + buf := bytes.NewBufferString(input) + s := make.NewScanner(buf) + + Expect(s.Scan()).To(BeTrueBecause("more to scan")) + Expect(s.Token()).To(Equal(token.IDENT)) + Expect(s.Literal()).To(Equal(strings.TrimSpace(input))) + Expect(s.Scan()).To(BeFalseBecause("at EOF")) + Expect(s.Token()).To(Equal(token.EOF)) + }, + ) + + DescribeTable("Scan identifier with trailing newline", + Entry(nil, "ident\n"), + Entry(nil, "./file/path\n"), + Entry(nil, "/abs/path\n"), + Entry(nil, "foo_bar\n"), + Entry(nil, "foo-bar\n"), + Entry(nil, "foo123\n"), + Entry(nil, "123\n"), + Entry(nil, "_foo\n"), + Entry(nil, "foo_\n"), + func(input string) { + buf := bytes.NewBufferString(input) + s := make.NewScanner(buf) + + Expect(s.Scan()).To(BeTrueBecause("more to scan")) + Expect(s.Token()).To(Equal(token.IDENT)) + Expect(s.Literal()).To(Equal(strings.TrimSpace(input))) + Expect(s.Scan()).To(BeFalseBecause("at EOF")) + Expect(s.Token()).To(Equal(token.EOF)) + }, + ) + + DescribeTable("Scan ident followed by token", + Entry(nil, "ident $"), + Entry(nil, "ident:"), + Entry(nil, "ident :"), + Entry(nil, "ident ="), + Entry(nil, "ident :="), + Entry(nil, "ident ::="), + Entry(nil, "ident :::="), + Entry(nil, "ident ?="), + Entry(nil, "ident !="), + Entry(nil, "ident ("), + Entry(nil, "ident )"), + Entry(nil, "ident {"), + Entry(nil, "ident }"), + Entry(nil, "ident ,"), + Entry(nil, "ident\n\t"), + func(input string) { + buf := bytes.NewBufferString(input) + s := make.NewScanner(buf) + + more := s.Scan() + + Expect(s.Token()).To(Equal(token.IDENT)) + Expect(s.Literal()).To(Equal("ident")) + Expect(more).To(BeTrueBecause("more to scan")) + }, + ) + + DescribeTable("Scan non-ident tokens", + Entry(nil, "$", token.DOLLAR), + Entry(nil, ":", token.COLON), + Entry(nil, "=", token.RECURSIVE_ASSIGN), + Entry(nil, ":=", token.SIMPLE_ASSIGN), + Entry(nil, "::=", token.POSIX_ASSIGN), + Entry(nil, ":::=", token.IMMEDIATE_ASSIGN), + Entry(nil, "?=", token.IFNDEF_ASSIGN), + Entry(nil, "!=", token.SHELL_ASSIGN), + Entry(nil, "(", token.LPAREN), + Entry(nil, ")", token.RPAREN), + Entry(nil, "{", token.LBRACE), + Entry(nil, "}", token.RBRACE), + Entry(nil, ",", token.COMMA), + Entry(nil, "\t", token.TAB), + func(input string, expected token.Token) { + buf := bytes.NewBufferString(input) + s := make.NewScanner(buf) + + more := s.Scan() + + Expect(s.Token()).To(Equal(expected)) + Expect(more).To(BeTrueBecause("more to scan")) + }, + ) + + It("should scan newline followed by token", func() { + buf := bytes.NewBufferString("\n ident") + s := make.NewScanner(buf) + + Expect(s.Scan()).To(BeTrue()) + Expect(s.Token()).To(Equal(token.NEWLINE)) + }) +}) diff --git a/token/position.go b/token/position.go new file mode 100644 index 0000000..22028ac --- /dev/null +++ b/token/position.go @@ -0,0 +1,10 @@ +package token + +import "go/token" + +type ( + Pos = token.Pos + Position = token.Position + File = token.File + FileSet = token.FileSet +) diff --git a/token/token.go b/token/token.go new file mode 100644 index 0000000..359ff3b --- /dev/null +++ b/token/token.go @@ -0,0 +1,289 @@ +// Package token defines constants representing the lexical tokens of a Makefile. +// It is based heavily on the [go/token package] +// +// [go/token package]: https://pkg.go.dev/go/token +package token + +import ( + "strconv" +) + +// Token defines the set of lexical tokens of a Makefile. +// [Quick Reference] +// +// [Quick Reference]: https://www.gnu.org/software/make/manual/html_node/Quick-Reference.html +type Token int + +const ( + UNSUPPORTED Token = -1 + ILLEGAL Token = iota + EOF + COMMENT // #comment text + + literal_beg + IDENT // some_name + literal_end + + operator_beg + // Operators and delimiters + LPAREN // ( + LBRACE // { + RPAREN // ) + RBRACE // } + DOLLAR // $ + COLON // : + COMMA // , + NEWLINE // \n + TAB // \t + + RECURSIVE_ASSIGN // = + SIMPLE_ASSIGN // := + POSIX_ASSIGN // ::= + IMMEDIATE_ASSIGN // :::= + IFNDEF_ASSIGN // ?= + SHELL_ASSIGN // != + operator_end + + directive_beg + // Directives + DEFINE // define + ENDEF // endef + UNDEFINE // undefine + IFDEF // ifdef + IFNDEF // ifndef + IFEQ // ifeq + IFNEQ // ifneq + ELSE // else + ENDIF // endif + INCLUDE // include + DASH_INCLUDE // -include + SINCLUDE // sinclude + OVERRIDE // override + EXPORT // export + UNEXPORT // unexport + PRIVATE // private + VPATH // vpath + directive_end + + function_beg + // Built-in functions + SUBST // $(subst from,to.text) + PATSUBST // $(patsubst pattern,replacement,text) + STRIP // $(strip string) + FINDSTRING // $(findstring find,text) + FILTER // $(filter patern...,text) + FILTER_OUT // $(filter-out patern...,text) + SORT // $(sort list) + WORD // $(word n,text) + WORDS // $(words text) + WORDLIST // $(wordlist s,e,text) + FIRSTWORD // $(firstword names...) + LASTWORD // $(lastword names...) + DIR // $(dir names...) + NOTDIR // $(notdir names...) + SUFFIX // $(suffix names...) + BASENAME // $(basename names...) + ADDSUFFIX // $(addsuffix suffix,names...) + ADDPREFIX // $(addprefix prefix,names...) + JOIN // $(join list1,list2) + WILDCARD // $(wildcard pattern...) + REALPATH // $(realpath names...) + ABSPATH // $(abspath names...) + ERROR // $(error text...) + WARNING // $(warning text...) + SHELL // $(shell command) + ORIGIN // $(origin variable) + FLAVOR // $(flavor variable) + LET // $(let var [var ...],words,text) + FOREACH // $(foreach var,words,text) + IF // $(if condition,then-part[,else-part]) + OR // $(or condition1[,condition2[,condition3…]]) + AND // $(and condition1[,condition2[,condition3…]]) + INTCMP // $(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]]) + CALL // $(call var,param,...) + EVAL // $(eval text) + FILE // $(file op filename,text) + VALUE // $(value var) + function_end +) + +var tokens = [...]string{ + ILLEGAL: "ILLEGAL", + EOF: "EOF", + COMMENT: "COMMENT", + IDENT: "IDENT", + + LPAREN: "(", + LBRACE: "{", + RPAREN: ")", + RBRACE: "}", + DOLLAR: "$", + COLON: ":", + COMMA: ",", + NEWLINE: "\n", + TAB: "\t", + + RECURSIVE_ASSIGN: "=", + SIMPLE_ASSIGN: ":=", + POSIX_ASSIGN: "::=", + IMMEDIATE_ASSIGN: ":::=", + IFNDEF_ASSIGN: "?=", + SHELL_ASSIGN: "!=", + + DEFINE: "define", + ENDEF: "endef", + UNDEFINE: "undefine", + IFDEF: "ifdef", + IFNDEF: "ifndef", + IFEQ: "ifeq", + IFNEQ: "ifneq", + ELSE: "else", + ENDIF: "endif", + INCLUDE: "include", + DASH_INCLUDE: "-include", + SINCLUDE: "sinclude", + OVERRIDE: "override", + EXPORT: "export", + UNEXPORT: "unexport", + PRIVATE: "private", + VPATH: "vpath", + + SUBST: "subst", + PATSUBST: "patsubst", + STRIP: "strip", + FINDSTRING: "findstring", + FILTER: "filter", + FILTER_OUT: "filter-out", + SORT: "sort", + WORD: "word", + WORDS: "words", + WORDLIST: "wordlist", + FIRSTWORD: "firstword", + LASTWORD: "lastword", + DIR: "dir", + NOTDIR: "notdir", + SUFFIX: "suffix", + BASENAME: "basename", + ADDSUFFIX: "addsuffix", + ADDPREFIX: "addprefix", + JOIN: "join", + WILDCARD: "wildcard", + REALPATH: "realpath", + ABSPATH: "abspath", + ERROR: "error", + WARNING: "warning", + SHELL: "shell", + ORIGIN: "origin", + FLAVOR: "flavor", + LET: "let", + FOREACH: "foreach", + IF: "if", + OR: "or", + AND: "and", + INTCMP: "intcmp", + CALL: "call", + EVAL: "eval", + FILE: "file", + VALUE: "value", +} + +// String returns the string corresponding to the token tok. +func (tok Token) String() string { + s := "" + if 0 <= tok && tok < Token(len(tokens)) { + s = tokens[tok] + } + if s == "" { + s = "token(" + strconv.Itoa(int(tok)) + ")" + } + + return s +} + +var ( + directives map[string]Token + functions map[string]Token + keywords map[string]Token +) + +func init() { + directives = make(map[string]Token, directive_end-(directive_beg+1)) + for i := directive_beg + 1; i < directive_end; i++ { + directives[tokens[i]] = i + } + + functions = make(map[string]Token, function_end-(function_beg+1)) + for i := function_beg + 1; i < function_end; i++ { + functions[tokens[i]] = i + } + + keywords = make(map[string]Token, len(directives)+len(functions)) + for k, v := range directives { + keywords[k] = v + } + for k, v := range functions { + keywords[k] = v + } +} + +func Lookup(ident string) Token { + if tok, ok := keywords[ident]; ok { + return tok + } + + return IDENT +} + +// IsLiteral returns true for tokens corresponding to identifiers. +func (tok Token) IsLiteral() bool { + return literal_beg < tok && tok < literal_end +} + +// IsOperator returns true for tokens corresponding to operators. +func (tok Token) IsOperator() bool { + return operator_beg < tok && tok < operator_end +} + +// IsDirective returns true for tokens corresponding to directives. +func (tok Token) IsDirective() bool { + return directive_beg < tok && tok < directive_end +} + +// IsBuiltinFunction returns true for tokens corresponding to built-in functions. +func (tok Token) IsBuiltinFunction() bool { + return function_beg < tok && tok < function_end +} + +// IsKeyword reports whether name is a keyword, that is, whether +// name is either a directive or a built-in function. +func IsKeyword(name string) bool { + return IsDirective(name) || IsBuiltinFunction(name) +} + +// IsDirective reports whether name is a directive. +func IsDirective(name string) bool { + _, ok := directives[name] + return ok +} + +// IsBuiltinFunction reports whether name is a built-in function. +func IsBuiltinFunction(name string) bool { + _, ok := functions[name] + return ok +} + +// IsIdentifier reports whether name is a valid identifier. +// Keywords are not identifiers. +func IsIdentifier(name string) bool { + if name == "" || IsKeyword(name) { + return false + } + switch name { + case "(", ")", "{", "}", "$", ":", ",", "\n", "\t": + fallthrough + case "=", ":=", "::=", ":::=", "?=", "!=": + return false + } + + return true +} diff --git a/token/token_suite_test.go b/token/token_suite_test.go new file mode 100644 index 0000000..a9beb41 --- /dev/null +++ b/token/token_suite_test.go @@ -0,0 +1,13 @@ +package token_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestToken(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Token Suite") +} diff --git a/token/token_test.go b/token/token_test.go new file mode 100644 index 0000000..97a415a --- /dev/null +++ b/token/token_test.go @@ -0,0 +1,545 @@ +package token_test + +import ( + "testing/quick" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/unmango/go-make/token" +) + +var Literals = []TableEntry{ + Entry(nil, token.IDENT), +} + +var Operators = []TableEntry{ + Entry(nil, token.LPAREN), + Entry(nil, token.LBRACE), + Entry(nil, token.RPAREN), + Entry(nil, token.RBRACE), + Entry(nil, token.DOLLAR), + Entry(nil, token.COLON), + Entry(nil, token.COMMA), + Entry(nil, token.NEWLINE), + Entry(nil, token.TAB), + Entry(nil, token.RECURSIVE_ASSIGN), + Entry(nil, token.SIMPLE_ASSIGN), + Entry(nil, token.POSIX_ASSIGN), + Entry(nil, token.IMMEDIATE_ASSIGN), + Entry(nil, token.IFNDEF_ASSIGN), + Entry(nil, token.SHELL_ASSIGN), +} + +var Directives = []TableEntry{ + Entry(nil, token.DEFINE), + Entry(nil, token.ENDEF), + Entry(nil, token.UNDEFINE), + Entry(nil, token.IFDEF), + Entry(nil, token.IFNDEF), + Entry(nil, token.IFEQ), + Entry(nil, token.IFNEQ), + Entry(nil, token.ELSE), + Entry(nil, token.ENDIF), + Entry(nil, token.INCLUDE), + Entry(nil, token.DASH_INCLUDE), + Entry(nil, token.SINCLUDE), + Entry(nil, token.OVERRIDE), + Entry(nil, token.EXPORT), + Entry(nil, token.UNEXPORT), + Entry(nil, token.PRIVATE), + Entry(nil, token.VPATH), +} + +var Functions = []TableEntry{ + Entry(nil, token.SUBST), + Entry(nil, token.PATSUBST), + Entry(nil, token.STRIP), + Entry(nil, token.FINDSTRING), + Entry(nil, token.FILTER), + Entry(nil, token.FILTER_OUT), + Entry(nil, token.SORT), + Entry(nil, token.WORD), + Entry(nil, token.WORDS), + Entry(nil, token.WORDLIST), + Entry(nil, token.FIRSTWORD), + Entry(nil, token.LASTWORD), + Entry(nil, token.DIR), + Entry(nil, token.NOTDIR), + Entry(nil, token.SUFFIX), + Entry(nil, token.BASENAME), + Entry(nil, token.ADDSUFFIX), + Entry(nil, token.ADDPREFIX), + Entry(nil, token.JOIN), + Entry(nil, token.WILDCARD), + Entry(nil, token.REALPATH), + Entry(nil, token.ABSPATH), + Entry(nil, token.ERROR), + Entry(nil, token.WARNING), + Entry(nil, token.SHELL), + Entry(nil, token.ORIGIN), + Entry(nil, token.FLAVOR), + Entry(nil, token.LET), + Entry(nil, token.FOREACH), + Entry(nil, token.IF), + Entry(nil, token.OR), + Entry(nil, token.AND), + Entry(nil, token.INTCMP), + Entry(nil, token.CALL), + Entry(nil, token.EVAL), + Entry(nil, token.FILE), + Entry(nil, token.VALUE), +} + +var _ = Describe("Token", func() { + Describe("IsLiteral", func() { + DescribeTable("true", Literals, + func(tok token.Token) { + Expect(tok.IsLiteral()).To(BeTrue()) + }, + ) + + DescribeTable("false", Operators, Directives, Functions, + func(tok token.Token) { + Expect(tok.IsLiteral()).To(BeFalse()) + }, + ) + }) + + Describe("IsOperator", func() { + DescribeTable("true", Operators, + func(tok token.Token) { + Expect(tok.IsOperator()).To(BeTrue()) + }, + ) + + DescribeTable("false", Literals, Directives, Functions, + func(tok token.Token) { + Expect(tok.IsOperator()).To(BeFalse()) + }, + ) + }) + + Describe("IsDirective", func() { + DescribeTable("true", Directives, + func(tok token.Token) { + Expect(tok.IsDirective()).To(BeTrue()) + }, + ) + + DescribeTable("false", Literals, Operators, Functions, + func(tok token.Token) { + Expect(tok.IsDirective()).To(BeFalse()) + }, + ) + }) + + Describe("IsBuiltinFunction", func() { + DescribeTable("true", Functions, + func(tok token.Token) { + Expect(tok.IsBuiltinFunction()).To(BeTrue()) + }, + ) + + DescribeTable("false", Literals, Directives, Operators, + func(tok token.Token) { + Expect(tok.IsBuiltinFunction()).To(BeFalse()) + }, + ) + }) + + DescribeTable("String", + Entry(nil, token.IDENT, "IDENT"), + Entry(nil, token.LPAREN, "("), + Entry(nil, token.LBRACE, "{"), + Entry(nil, token.RPAREN, ")"), + Entry(nil, token.RBRACE, "}"), + Entry(nil, token.DOLLAR, "$"), + Entry(nil, token.COLON, ":"), + Entry(nil, token.COMMA, ","), + Entry(nil, token.NEWLINE, "\n"), + Entry(nil, token.TAB, "\t"), + Entry(nil, token.RECURSIVE_ASSIGN, "="), + Entry(nil, token.SIMPLE_ASSIGN, ":="), + Entry(nil, token.POSIX_ASSIGN, "::="), + Entry(nil, token.IMMEDIATE_ASSIGN, ":::="), + Entry(nil, token.IFNDEF_ASSIGN, "?="), + Entry(nil, token.SHELL_ASSIGN, "!="), + Entry(nil, token.DEFINE, "define"), + Entry(nil, token.ENDEF, "endef"), + Entry(nil, token.UNDEFINE, "undefine"), + Entry(nil, token.IFDEF, "ifdef"), + Entry(nil, token.IFNDEF, "ifndef"), + Entry(nil, token.IFEQ, "ifeq"), + Entry(nil, token.IFNEQ, "ifneq"), + Entry(nil, token.ELSE, "else"), + Entry(nil, token.ENDIF, "endif"), + Entry(nil, token.INCLUDE, "include"), + Entry(nil, token.DASH_INCLUDE, "-include"), + Entry(nil, token.SINCLUDE, "sinclude"), + Entry(nil, token.OVERRIDE, "override"), + Entry(nil, token.EXPORT, "export"), + Entry(nil, token.UNEXPORT, "unexport"), + Entry(nil, token.PRIVATE, "private"), + Entry(nil, token.VPATH, "vpath"), + Entry(nil, token.SUBST, "subst"), + Entry(nil, token.PATSUBST, "patsubst"), + Entry(nil, token.STRIP, "strip"), + Entry(nil, token.FINDSTRING, "findstring"), + Entry(nil, token.FILTER, "filter"), + Entry(nil, token.FILTER_OUT, "filter-out"), + Entry(nil, token.SORT, "sort"), + Entry(nil, token.WORD, "word"), + Entry(nil, token.WORDS, "words"), + Entry(nil, token.WORDLIST, "wordlist"), + Entry(nil, token.FIRSTWORD, "firstword"), + Entry(nil, token.LASTWORD, "lastword"), + Entry(nil, token.DIR, "dir"), + Entry(nil, token.NOTDIR, "notdir"), + Entry(nil, token.SUFFIX, "suffix"), + Entry(nil, token.BASENAME, "basename"), + Entry(nil, token.ADDSUFFIX, "addsuffix"), + Entry(nil, token.ADDPREFIX, "addprefix"), + Entry(nil, token.JOIN, "join"), + Entry(nil, token.WILDCARD, "wildcard"), + Entry(nil, token.REALPATH, "realpath"), + Entry(nil, token.ABSPATH, "abspath"), + Entry(nil, token.ERROR, "error"), + Entry(nil, token.WARNING, "warning"), + Entry(nil, token.SHELL, "shell"), + Entry(nil, token.ORIGIN, "origin"), + Entry(nil, token.FLAVOR, "flavor"), + Entry(nil, token.LET, "let"), + Entry(nil, token.FOREACH, "foreach"), + Entry(nil, token.IF, "if"), + Entry(nil, token.OR, "or"), + Entry(nil, token.AND, "and"), + Entry(nil, token.INTCMP, "intcmp"), + Entry(nil, token.CALL, "call"), + Entry(nil, token.EVAL, "eval"), + Entry(nil, token.FILE, "file"), + Entry(nil, token.VALUE, "value"), + Entry(nil, token.Token(420), "token(420)"), + func(tok token.Token, expected string) { + Expect(tok.String()).To(Equal(expected)) + }, + ) + + Describe("Lookup", func() { + DescribeTable("Lookup keyword", + Entry(nil, token.DEFINE, "define"), + Entry(nil, token.ENDEF, "endef"), + Entry(nil, token.UNDEFINE, "undefine"), + Entry(nil, token.IFDEF, "ifdef"), + Entry(nil, token.IFNDEF, "ifndef"), + Entry(nil, token.IFEQ, "ifeq"), + Entry(nil, token.IFNEQ, "ifneq"), + Entry(nil, token.ELSE, "else"), + Entry(nil, token.ENDIF, "endif"), + Entry(nil, token.INCLUDE, "include"), + Entry(nil, token.DASH_INCLUDE, "-include"), + Entry(nil, token.SINCLUDE, "sinclude"), + Entry(nil, token.OVERRIDE, "override"), + Entry(nil, token.EXPORT, "export"), + Entry(nil, token.UNEXPORT, "unexport"), + Entry(nil, token.PRIVATE, "private"), + Entry(nil, token.VPATH, "vpath"), + Entry(nil, token.SUBST, "subst"), + Entry(nil, token.PATSUBST, "patsubst"), + Entry(nil, token.STRIP, "strip"), + Entry(nil, token.FINDSTRING, "findstring"), + Entry(nil, token.FILTER, "filter"), + Entry(nil, token.FILTER_OUT, "filter-out"), + Entry(nil, token.SORT, "sort"), + Entry(nil, token.WORD, "word"), + Entry(nil, token.WORDS, "words"), + Entry(nil, token.WORDLIST, "wordlist"), + Entry(nil, token.FIRSTWORD, "firstword"), + Entry(nil, token.LASTWORD, "lastword"), + Entry(nil, token.DIR, "dir"), + Entry(nil, token.NOTDIR, "notdir"), + Entry(nil, token.SUFFIX, "suffix"), + Entry(nil, token.BASENAME, "basename"), + Entry(nil, token.ADDSUFFIX, "addsuffix"), + Entry(nil, token.ADDPREFIX, "addprefix"), + Entry(nil, token.JOIN, "join"), + Entry(nil, token.WILDCARD, "wildcard"), + Entry(nil, token.REALPATH, "realpath"), + Entry(nil, token.ABSPATH, "abspath"), + Entry(nil, token.ERROR, "error"), + Entry(nil, token.WARNING, "warning"), + Entry(nil, token.SHELL, "shell"), + Entry(nil, token.ORIGIN, "origin"), + Entry(nil, token.FLAVOR, "flavor"), + Entry(nil, token.LET, "let"), + Entry(nil, token.FOREACH, "foreach"), + Entry(nil, token.IF, "if"), + Entry(nil, token.OR, "or"), + Entry(nil, token.AND, "and"), + Entry(nil, token.INTCMP, "intcmp"), + Entry(nil, token.CALL, "call"), + Entry(nil, token.EVAL, "eval"), + Entry(nil, token.FILE, "file"), + Entry(nil, token.VALUE, "value"), + func(tok token.Token, keyword string) { + Expect(token.Lookup(keyword)).To(Equal(tok)) + }, + ) + + It("should lookup identifiers", func() { + err := quick.Check(func(s string) bool { + return token.Lookup(s) == token.IDENT + }, nil) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("IsKeyword", func() { + DescribeTable("keywords", + Entry(nil, "define"), + Entry(nil, "endef"), + Entry(nil, "undefine"), + Entry(nil, "ifdef"), + Entry(nil, "ifndef"), + Entry(nil, "ifeq"), + Entry(nil, "ifneq"), + Entry(nil, "else"), + Entry(nil, "endif"), + Entry(nil, "include"), + Entry(nil, "-include"), + Entry(nil, "sinclude"), + Entry(nil, "override"), + Entry(nil, "export"), + Entry(nil, "unexport"), + Entry(nil, "private"), + Entry(nil, "vpath"), + Entry(nil, "subst"), + Entry(nil, "patsubst"), + Entry(nil, "strip"), + Entry(nil, "findstring"), + Entry(nil, "filter"), + Entry(nil, "filter-out"), + Entry(nil, "sort"), + Entry(nil, "word"), + Entry(nil, "words"), + Entry(nil, "wordlist"), + Entry(nil, "firstword"), + Entry(nil, "lastword"), + Entry(nil, "dir"), + Entry(nil, "notdir"), + Entry(nil, "suffix"), + Entry(nil, "basename"), + Entry(nil, "addsuffix"), + Entry(nil, "addprefix"), + Entry(nil, "join"), + Entry(nil, "wildcard"), + Entry(nil, "realpath"), + Entry(nil, "abspath"), + Entry(nil, "error"), + Entry(nil, "warning"), + Entry(nil, "shell"), + Entry(nil, "origin"), + Entry(nil, "flavor"), + Entry(nil, "let"), + Entry(nil, "foreach"), + Entry(nil, "if"), + Entry(nil, "or"), + Entry(nil, "and"), + Entry(nil, "intcmp"), + Entry(nil, "call"), + Entry(nil, "eval"), + Entry(nil, "file"), + Entry(nil, "value"), + func(keyword string) { + Expect(token.IsKeyword(keyword)).To(BeTrue()) + }, + ) + + It("should return false for non-keywords", func() { + err := quick.Check(func(s string) bool { + return !token.IsKeyword(s) + }, nil) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("IsDirective", func() { + DescribeTable("directives", + Entry(nil, "define"), + Entry(nil, "endef"), + Entry(nil, "undefine"), + Entry(nil, "ifdef"), + Entry(nil, "ifndef"), + Entry(nil, "ifeq"), + Entry(nil, "ifneq"), + Entry(nil, "else"), + Entry(nil, "endif"), + Entry(nil, "include"), + Entry(nil, "-include"), + Entry(nil, "sinclude"), + Entry(nil, "override"), + Entry(nil, "export"), + Entry(nil, "unexport"), + Entry(nil, "private"), + Entry(nil, "vpath"), + func(directive string) { + Expect(token.IsDirective(directive)).To(BeTrue()) + }, + ) + + It("should return false for non-directive", func() { + err := quick.Check(func(s string) bool { + return !token.IsDirective(s) + }, nil) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("IsBuiltinFunction", func() { + DescribeTable("functions", + Entry(nil, "subst"), + Entry(nil, "patsubst"), + Entry(nil, "strip"), + Entry(nil, "findstring"), + Entry(nil, "filter"), + Entry(nil, "filter-out"), + Entry(nil, "sort"), + Entry(nil, "word"), + Entry(nil, "words"), + Entry(nil, "wordlist"), + Entry(nil, "firstword"), + Entry(nil, "lastword"), + Entry(nil, "dir"), + Entry(nil, "notdir"), + Entry(nil, "suffix"), + Entry(nil, "basename"), + Entry(nil, "addsuffix"), + Entry(nil, "addprefix"), + Entry(nil, "join"), + Entry(nil, "wildcard"), + Entry(nil, "realpath"), + Entry(nil, "abspath"), + Entry(nil, "error"), + Entry(nil, "warning"), + Entry(nil, "shell"), + Entry(nil, "origin"), + Entry(nil, "flavor"), + Entry(nil, "let"), + Entry(nil, "foreach"), + Entry(nil, "if"), + Entry(nil, "or"), + Entry(nil, "and"), + Entry(nil, "intcmp"), + Entry(nil, "call"), + Entry(nil, "eval"), + Entry(nil, "file"), + Entry(nil, "value"), + func(function string) { + Expect(token.IsBuiltinFunction(function)).To(BeTrue()) + }, + ) + + It("should return false for non-functions", func() { + err := quick.Check(func(s string) bool { + return !token.IsBuiltinFunction(s) + }, nil) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("IsIdentifier", func() { + DescribeTable("keywords", + Entry(nil, "define"), + Entry(nil, "endef"), + Entry(nil, "undefine"), + Entry(nil, "ifdef"), + Entry(nil, "ifndef"), + Entry(nil, "ifeq"), + Entry(nil, "ifneq"), + Entry(nil, "else"), + Entry(nil, "endif"), + Entry(nil, "include"), + Entry(nil, "-include"), + Entry(nil, "sinclude"), + Entry(nil, "override"), + Entry(nil, "export"), + Entry(nil, "unexport"), + Entry(nil, "private"), + Entry(nil, "vpath"), + Entry(nil, "subst"), + Entry(nil, "patsubst"), + Entry(nil, "strip"), + Entry(nil, "findstring"), + Entry(nil, "filter"), + Entry(nil, "filter-out"), + Entry(nil, "sort"), + Entry(nil, "word"), + Entry(nil, "words"), + Entry(nil, "wordlist"), + Entry(nil, "firstword"), + Entry(nil, "lastword"), + Entry(nil, "dir"), + Entry(nil, "notdir"), + Entry(nil, "suffix"), + Entry(nil, "basename"), + Entry(nil, "addsuffix"), + Entry(nil, "addprefix"), + Entry(nil, "join"), + Entry(nil, "wildcard"), + Entry(nil, "realpath"), + Entry(nil, "abspath"), + Entry(nil, "error"), + Entry(nil, "warning"), + Entry(nil, "shell"), + Entry(nil, "origin"), + Entry(nil, "flavor"), + Entry(nil, "let"), + Entry(nil, "foreach"), + Entry(nil, "if"), + Entry(nil, "or"), + Entry(nil, "and"), + Entry(nil, "intcmp"), + Entry(nil, "call"), + Entry(nil, "eval"), + Entry(nil, "file"), + Entry(nil, "value"), + Entry(nil, ""), + Entry(nil, "("), + Entry(nil, ")"), + Entry(nil, "{"), + Entry(nil, "}"), + Entry(nil, ":"), + Entry(nil, "$"), + Entry(nil, ","), + Entry(nil, "="), + Entry(nil, ":="), + Entry(nil, "::="), + Entry(nil, ":::="), + Entry(nil, "\n"), + Entry(nil, "\t"), + Entry(nil, "?="), + Entry(nil, "!="), + func(keyword string) { + Expect(token.IsIdentifier(keyword)).To(BeFalse()) + }, + ) + + DescribeTable("should return true for non-keywords", + Entry(nil, "b"), + Entry(nil, "blah"), + Entry(nil, "foo"), + Entry(nil, "foo-bar"), + Entry(nil, "foo_bar"), + Entry(nil, "./file/path"), + Entry(nil, "/abs/path"), + Entry(nil, "12"), + func(s string) { + Expect(token.IsIdentifier(s)).To(BeTrue()) + }, + ) + }) +}) diff --git a/write.go b/write.go new file mode 100644 index 0000000..bd0a091 --- /dev/null +++ b/write.go @@ -0,0 +1,106 @@ +package make + +import ( + "fmt" + "io" + "strings" +) + +type Writer struct { + w io.Writer +} + +func NewWriter(w io.Writer) *Writer { + return &Writer{w} +} + +func (w *Writer) WriteLine() (n int, err error) { + return io.WriteString(w.w, "\n") +} + +func (w *Writer) WriteMakefile(m Makefile) (n int, err error) { + for _, r := range m.Rules { + if x, err := w.WriteRule(r); err != nil { + return 0, err + } else { + n += x + } + } + + return +} + +func (w *Writer) WritePreReq(p string) (n int, err error) { + return io.WriteString(w.w, " "+p) +} + +func (w *Writer) WritePreReqs(ps []string) (n int, err error) { + for _, p := range ps { + if x, err := w.WritePreReq(p); err != nil { + return 0, err + } else { + n += x + } + } + + return +} + +func (w *Writer) WriteRecipe(r string) (n int, err error) { + return io.WriteString(w.w, "\t"+r) +} + +func (w *Writer) WriteRecipes(rs []string) (n int, err error) { + for _, p := range rs { + if x, err := w.WriteRecipe(p); err != nil { + return 0, err + } else { + n += x + } + } + + return +} + +func (w *Writer) WriteRule(r Rule) (n int, err error) { + if len(r.Target) == 0 { + return 0, fmt.Errorf("no targets") + } + + if n, err = w.WriteTargets(r.Target); err != nil { + return + } + if x, err := w.WritePreReqs(r.PreReqs); err != nil { + return 0, err + } else { + n += x + } + if x, err := w.WriteLine(); err != nil { + return 0, err + } else { + n += x + } + if x, err := w.WriteRecipes(r.Recipe); err != nil { + return 0, err + } else { + n += x + } + if len(r.Recipe) > 0 { + if x, err := w.WriteLine(); err != nil { + return 0, err + } else { + return n + x, nil + } + } + + return +} + +func (w *Writer) WriteTarget(t string) (n int, err error) { + return io.WriteString(w.w, t+":") +} + +func (w *Writer) WriteTargets(ts []string) (n int, err error) { + t := strings.Join(ts, " ") + return io.WriteString(w.w, t+":") +} diff --git a/write_test.go b/write_test.go new file mode 100644 index 0000000..a79f31f --- /dev/null +++ b/write_test.go @@ -0,0 +1,108 @@ +package make_test + +import ( + "bytes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/unmango/go-make" +) + +var _ = Describe("Write", func() { + It("should write a line", func() { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + n, err := w.WriteLine() + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal("\n")) + Expect(n).To(Equal(1)) + }) + + It("should write a target", func() { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + n, err := w.WriteTarget("target") + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal("target:")) + Expect(n).To(Equal(7)) + }) + + It("should write multiple targets", func() { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + n, err := w.WriteTargets([]string{"target", "target2"}) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal("target target2:")) + Expect(n).To(Equal(15)) + }) + + DescribeTable("Rules", + Entry(nil, + make.Rule{Target: []string{"target"}}, + "target:\n", + ), + Entry(nil, + make.Rule{Target: []string{"target", "target2"}}, + "target target2:\n", + ), + Entry(nil, + make.Rule{ + Target: []string{"target"}, + PreReqs: []string{"prereq"}, + }, + "target: prereq\n", + ), + Entry(nil, + make.Rule{ + Target: []string{"target"}, + PreReqs: []string{"prereq"}, + Recipe: []string{"curl https://example.com"}, + }, + "target: prereq\n\tcurl https://example.com\n", + ), + Entry(nil, + make.Rule{ + Target: []string{"target"}, + Recipe: []string{"curl https://example.com"}, + }, + "target:\n\tcurl https://example.com\n", + ), + func(r make.Rule, expected string) { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + n, err := w.WriteRule(r) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + Expect(n).To(Equal(len(expected))) + }, + ) + + It("should error when rule has no targets", func() { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + _, err := w.WriteRule(make.Rule{}) + + Expect(err).To(MatchError("no targets")) + }) + + It("should write multiple rules", func() { + buf := &bytes.Buffer{} + w := make.NewWriter(buf) + + _, err := w.WriteRule(make.Rule{Target: []string{"target"}}) + Expect(err).NotTo(HaveOccurred()) + _, err = w.WriteRule(make.Rule{Target: []string{"target2"}}) + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal("target:\ntarget2:\n")) + }) +})