[
  {
    "path": ".github/FUNDING.yml",
    "content": "---\ngithub: [dnaeon]\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "---\nversion: 2\nupdates:\n  # Create PRs for dependency updates\n  - package-ecosystem: gomod\n    directory: /\n    schedule:\n      interval: weekly\n    commit-message:\n      prefix: \"deps:\"\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "# .github/workflows/test.yaml\non: [push, pull_request]\nname: test\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [1.23.x, 1.24.x]\n        os: [ubuntu-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n    - uses: actions/setup-go@v6\n      with:\n        go-version: ${{ matrix.go-version }}\n    - uses: actions/checkout@v6\n    - run: make test-cover\n    - name: Upload coverage reports to Codecov\n      uses: codecov/codecov-action@v6.0.0\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n        slug: dnaeon/makefile-graph\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore Emacs backup files\n*~\nbin/\ncoverage.txt\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n  1. Redistributions of source code must retain the above copyright\n     notice, this list of conditions and the following disclaimer.\n  2. Redistributions in binary form must reproduce the above copyright\n     notice, this list of conditions and the following disclaimer in the\n     documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Makefile",
    "content": ".DEFAULT_GOAL := build\nLOCAL_BIN ?= $(shell pwd)/bin\nBINARY ?= $(LOCAL_BIN)/makefile-graph\nSRC_DIRS := $(shell go list -f '{{.Dir}}' ./...)\n\n$(LOCAL_BIN):\n\tmkdir -p $(LOCAL_BIN)\n\n$(BINARY): $(SRC_DIRS) | $(LOCAL_BIN)\n\tgo build -o $(BINARY) cmd/makefile-graph/main.go\n\nbuild: $(BINARY)\n\nget:\n\tgo get -v -t -d ./...\n\ntest:\n\tgo test -v -race $(shell go list ./... | grep -v -E 'cmd|fixtures')\n\ntest-cover:\n\tgo test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -v -E 'cmd|fixtures')\n\n.PHONY: get test test-cover build\n"
  },
  {
    "path": "README.md",
    "content": "# makefile-graph\n\n[![Build Status](https://github.com/dnaeon/makefile-graph/actions/workflows/test.yaml/badge.svg)](https://github.com/dnaeon/makefile-graph/actions/workflows/test.yaml/badge.svg)\n[![Go Reference](https://pkg.go.dev/badge/github.com/dnaeon/makefile-graph.svg)](https://pkg.go.dev/github.com/dnaeon/makefile-graph)\n[![Go Report Card](https://goreportcard.com/badge/github.com/dnaeon/makefile-graph)](https://goreportcard.com/report/github.com/dnaeon/makefile-graph)\n[![codecov](https://codecov.io/gh/dnaeon/makefile-graph/branch/master/graph/badge.svg)](https://codecov.io/gh/dnaeon/makefile-graph)\n\n`makefile-graph` is a Go module and CLI application, which parses\n[GNU Make](https://www.gnu.org/software/make/)'s internal database and generates a\ngraph representing the relationships between the discovered Makefile targets.\n\n![echarts dark demo](./images/echarts-dark.gif)\n\n## Requirements\n\n* [GNU Make](https://www.gnu.org/software/make/)\n* Go version 1.21.x or later\n\n## Installation\n\nYou can install the CLI application using one of the following ways.\n\nIf you have cloned the repository you can build the CLI app using the provided\nMakefile target.\n\n``` shell\nmake build\n```\n\nThe resulting binary will be located in `bin/makefile-graph`.\n\nInstall the CLI application using `go install`.\n\n``` shell\ngo install github.com/dnaeon/makefile-graph/cmd/makefile-graph@latest\n```\n\nIn order to install the parser package and use it in your own Go code run the\nfollowing command within your Go module.\n\n``` shell\ngo get -v github.com/dnaeon/makefile-graph/pkg/parser\n```\n\n## Usage\n\nLet's use the following [example\nMakefile](https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html)\nfrom [GNU make's documentation](https://www.gnu.org/software/make/manual/make.html).\n\n``` makefile\nedit : main.o kbd.o command.o display.o \\\n       insert.o search.o files.o utils.o\n\tcc -o edit main.o kbd.o command.o display.o \\\n\t\t   insert.o search.o files.o utils.o\n\nmain.o : main.c defs.h\n\tcc -c main.c\nkbd.o : kbd.c defs.h command.h\n\tcc -c kbd.c\ncommand.o : command.c defs.h command.h\n\tcc -c command.c\ndisplay.o : display.c defs.h buffer.h\n\tcc -c display.c\ninsert.o : insert.c defs.h buffer.h\n\tcc -c insert.c\nsearch.o : search.c defs.h buffer.h\n\tcc -c search.c\nfiles.o : files.c defs.h buffer.h command.h\n\tcc -c files.c\nutils.o : utils.c defs.h\n\tcc -c utils.c\nclean :\n\trm edit main.o kbd.o command.o display.o \\\n\t   insert.o search.o files.o utils.o\n```\n\nYou can also find this example Makefile in the [examples](./examples/) directory\nof this repo.\n\nRunning the following command will generate the [Dot\nrepresentation](https://graphviz.org/doc/info/lang.html) for the Makefile\ntargets and their dependencies from our example Makefile.\n\n``` shell\nmakefile-graph --makefile examples/Makefile --direction TB\n```\n\nIn order to render the graph you can pipe it directly to the\n[dot command](https://graphviz.org/doc/info/command.html), e.g.\n\n``` makefile\nmakefile-graph --makefile examples/Makefile --direction TB | dot -Tsvg -o graph.svg\n```\n\nThis is what the graph looks like when we render it using `dot(1)`.\n\n![Example Makefile Graph](./images/image-1.svg)\n\nSometimes when rendering large graphs it may not be obvious at first glance what\nare the dependencies for a specific target. In order to help with such\nsituations `makefile-graph` supports a flag which allows you to highlight\nspecific targets and their dependencies.\n\nThe following command will highlight the `files.o` target along with it's\ndependencies.\n\n``` makefile\nmakefile-graph \\\n    --makefile examples/Makefile \\\n    --direction TB \\\n    --target files.o \\\n    --highlight \\\n    --highlight-color lightgreen\n```\n\nWhen we render the output from above command we will see this graph\nrepresentation.\n\n![Example Makefile Graph Highlighted](./images/image-2.svg)\n\nIf we want to focus on a specific target and it's dependencies only we can use\nthe following command, which will generate a graph only for the target and it's\ndependencies.\n\n``` makefile\nmakefile-graph \\\n    --makefile examples/Makefile \\\n    --direction TB \\\n    --target files.o \\\n    --related-only\n```\n\nThis is what the resulting graph looks like.\n\n![Example Makefile Graph Related Only](./images/image-3.svg)\n\nThe `--direction` option is used for specifying the [direction of graph\nlayout](https://graphviz.org/docs/attrs/rankdir/). You can set it to `TB`, `BT`,\n`LR` or `RL`.\n\nThe `--format` option is used for specifying the output format for the graph. By\ndefault it will produce the `dot` representation for the graph.\n\nYou can also view the [topological\norder](https://en.wikipedia.org/wiki/Topological_sorting) for a given target by\nsetting the format to `tsort`, e.g.\n\n``` makefile\nmakefile-graph \\\n    --makefile examples/Makefile \\\n    --target files.o \\\n    --related-only \\\n    --format tsort\n```\n\nRunning above command produces the following output, which represents the\ntopological order for the `files.o` target.\n\n``` text\ndefs.h\nbuffer.h\ncommand.h\nfiles.c\nfiles.o\n```\n\nThe graph can also be rendered with [Apache ECharts](https://echarts.apache.org/en/index.html)\nusing the [go-echarts](https://github.com/go-echarts/go-echarts) library.\n\nThe following command will generate an HTML file, which renders the graph in\nApache ECharts.\n\n``` shell\nmakefile-graph \\\n    --makefile examples/Makefile \\\n    --direction LR \\\n    --theme dark \\\n    --format echarts\n```\n\n![echarts dark demo](./images/echarts-dark.gif)\n\nA different theme can be specified using the `--theme` option. This example\nrenders the same graph using `--theme=white`.\n\n![echarts white demo](./images/echarts-white.gif)\n\n## Tests\n\nRun the tests.\n\n``` shell\nmake test\n```\n\nRun test coverage.\n\n``` shell\nmake test-cover\n```\n\n## Contributing\n\n`makefile-graph` is hosted on\n[Github](https://github.com/dnaeon/makefile-graph). Please contribute by\nreporting issues, suggesting features or by sending patches using pull requests.\n\n## License\n\n`makefile-graph` is Open Source and licensed under the [BSD\nLicense](http://opensource.org/licenses/BSD-2-Clause).\n"
  },
  {
    "path": "cmd/makefile-graph/main.go",
    "content": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions\n// are met:\n//\n//   1. Redistributions of source code must retain the above copyright\n//      notice, this list of conditions and the following disclaimer.\n//   2. Redistributions in binary form must reproduce the above copyright\n//      notice, this list of conditions and the following disclaimer in the\n//      documentation and/or other materials provided with the distribution.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n// POSSIBILITY OF SUCH DAMAGE.\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/go-echarts/go-echarts/v2/charts\"\n\t\"github.com/go-echarts/go-echarts/v2/components\"\n\t\"github.com/go-echarts/go-echarts/v2/opts\"\n\t\"gopkg.in/dnaeon/go-graph.v1\"\n\n\t\"github.com/dnaeon/makefile-graph/pkg/parser\"\n)\n\nvar errNoTargetName = errors.New(\"Must specify target name\")\nvar errInvalidLayoutDirection = errors.New(\"Invalid layout direction\")\nvar errInvalidFormat = errors.New(\"Invalid format specified\")\n\nconst (\n\tformatDot      = \"dot\"\n\tformatTopoSort = \"tsort\"\n\tformatEcharts  = \"echarts\"\n)\n\nfunc main() {\n\tvar relatedOnly, highlight bool\n\tvar makefile, target, highlightColor, direction, format, theme string\n\tvar fontSize float64\n\tvar fontStyle string\n\n\tflag.StringVar(&makefile, \"makefile\", \"Makefile\", \"path to Makefile\")\n\tflag.StringVar(&target, \"target\", \"\", \"name of a target\")\n\tflag.BoolVar(&highlight, \"highlight\", false, \"highlight target and related targets\")\n\tflag.StringVar(&highlightColor, \"highlight-color\", \"green\", \"color to use for highlighting\")\n\tflag.BoolVar(&relatedOnly, \"related-only\", false, \"return only related vertices for a target\")\n\tflag.StringVar(&direction, \"direction\", \"TB\", \"layout direction: TB, BT, LR or RL\")\n\tflag.StringVar(&format, \"format\", \"dot\", \"format to use: dot, tsort or echarts\")\n\tflag.StringVar(&theme, \"theme\", \"default\", \"echarts theme to use, e.g. white, dark, vintage, etc.\")\n\tflag.Float64Var(&fontSize, \"font-size\", 0.0, \"echarts font size\")\n\tflag.StringVar(&fontStyle, \"font-style\", \"normal\", \"echarts font style, e.g. normal, italic, oblique\")\n\n\tflag.Parse()\n\n\t// What format to print the graph in: Dot representation or topo sort\n\tformats := []string{formatDot, formatTopoSort, formatEcharts}\n\tif !slices.Contains(formats, format) {\n\t\tprintErrAndExit(errInvalidFormat)\n\t}\n\n\t// Valid directions\n\tdirections := []string{\"TB\", \"BT\", \"LR\", \"RL\"}\n\tdirection = strings.ToUpper(direction)\n\tif !slices.Contains(directions, direction) {\n\t\tprintErrAndExit(errInvalidLayoutDirection)\n\t}\n\n\tif relatedOnly && target == \"\" {\n\t\tprintErrAndExit(errNoTargetName)\n\t}\n\n\tif highlight && target == \"\" {\n\t\tprintErrAndExit(errNoTargetName)\n\t}\n\n\tinfo, err := os.Stat(makefile)\n\tif err != nil {\n\t\tprintErrAndExit(err)\n\t}\n\tif info.IsDir() {\n\t\tprintErrAndExit(fmt.Errorf(\"Invalid Makefile: %s is a directory\", makefile))\n\t}\n\n\t// Dump and parse the db\n\treader, err := dumpMakeDb(makefile)\n\tif err != nil {\n\t\tprintErrAndExit(err)\n\t}\n\n\tp := parser.New()\n\tg, err := p.Parse(reader)\n\tif err != nil {\n\t\tprintErrAndExit(err)\n\t}\n\n\t// Set layout direction (applicable for Dot format only)\n\tattrs := g.GetDotAttributes()\n\tattrs[\"rankdir\"] = direction\n\n\t// Highlight, if requested\n\tif highlight {\n\t\tif err := highlightVertices(g, target, highlightColor); err != nil {\n\t\t\tprintErrAndExit(err)\n\t\t}\n\t}\n\n\t// Keep only vertices related to the specified target\n\tif relatedOnly {\n\t\tif err := keepRelatedVerticesOnly(g, target); err != nil {\n\t\t\tprintErrAndExit(err)\n\t\t}\n\t}\n\n\tswitch format {\n\tcase formatDot:\n\t\tif err := graph.WriteDot(g, os.Stdout); err != nil {\n\t\t\tprintErrAndExit(err)\n\t\t}\n\tcase formatTopoSort:\n\t\tcollector := g.NewCollector()\n\t\tif err := graph.WalkTopoOrder(g, collector.WalkFunc); err != nil {\n\t\t\tprintErrAndExit(err)\n\t\t}\n\t\tfor _, v := range collector.Get() {\n\t\t\tfmt.Println(v.Value)\n\t\t}\n\tcase formatEcharts:\n\t\tglobalOpts := []charts.GlobalOpts{\n\t\t\tcharts.WithInitializationOpts(\n\t\t\t\topts.Initialization{\n\t\t\t\t\tWidth:  \"100%\",\n\t\t\t\t\tHeight: \"95vh\",\n\t\t\t\t\tTheme:  theme,\n\t\t\t\t},\n\t\t\t),\n\t\t\tcharts.WithTooltipOpts(opts.Tooltip{Show: opts.Bool(true)}),\n\t\t}\n\t\tseriesOpts := []charts.SeriesOpts{\n\t\t\tcharts.WithTreeOpts(\n\t\t\t\topts.TreeChart{\n\t\t\t\t\tRoam:              opts.Bool(true),\n\t\t\t\t\tExpandAndCollapse: opts.Bool(true),\n\t\t\t\t\tSymbolKeepAspect:  opts.Bool(true),\n\t\t\t\t\tLayout:            \"orthogonal\",\n\t\t\t\t\tOrient:            direction,\n\t\t\t\t\tInitialTreeDepth:  2,\n\t\t\t\t\tLeaves: &opts.TreeLeaves{\n\t\t\t\t\t\tLabel: &opts.Label{\n\t\t\t\t\t\t\tShow:      opts.Bool(true),\n\t\t\t\t\t\t\tPosition:  \"top\",\n\t\t\t\t\t\t\tFontSize:  float32(fontSize),\n\t\t\t\t\t\t\tFontStyle: fontStyle,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\tcharts.WithLabelOpts(opts.Label{\n\t\t\t\tShow:      opts.Bool(true),\n\t\t\t\tPosition:  \"top\",\n\t\t\t\tFontSize:  float32(fontSize),\n\t\t\t\tFontStyle: fontStyle,\n\t\t\t}),\n\t\t}\n\t\tif err := writeEchartsTree(g, os.Stdout, globalOpts, seriesOpts); err != nil {\n\t\t\tprintErrAndExit(err)\n\t\t}\n\t}\n}\n\n// keepRelatedVerticesOnly removes all vertices from the graph, which are not\n// reachable from the given source vertex.\nfunc keepRelatedVerticesOnly(g graph.Graph[string], source string) error {\n\t// A dummy walker which we use only so that we can paint the vertices.\n\t// The ones which remain graph.White are not related to our source\n\t// vertex, since they are not reachable from it.\n\tdummyWalker := func(*graph.Vertex[string]) error {\n\t\treturn nil\n\t}\n\tif err := graph.WalkPostOrderDFS(g, source, dummyWalker); err != nil {\n\t\treturn err\n\t}\n\n\ttoRemove := make([]*graph.Vertex[string], 0)\n\tfor _, v := range g.GetVertices() {\n\t\tif v.Color == graph.White {\n\t\t\ttoRemove = append(toRemove, v)\n\t\t}\n\t}\n\tfor _, v := range toRemove {\n\t\tg.DeleteVertex(v.Value)\n\t}\n\n\treturn nil\n}\n\n// highlightTarget colors all vertices reachable from source with the given\n// color.\nfunc highlightVertices(g graph.Graph[string], source string, color string) error {\n\twalker := func(v *graph.Vertex[string]) error {\n\t\t// Dot attributes\n\t\tv.DotAttributes[\"color\"] = color\n\t\tv.DotAttributes[\"fillcolor\"] = color\n\n\t\t// Echarts attributes\n\t\tv.EchartsStyle = &opts.ItemStyle{\n\t\t\tColor: color,\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn graph.WalkPostOrderDFS(g, source, walker)\n}\n\n// printErrAndExit prints the given error and calls [os.Exit]\nfunc printErrAndExit(err error) {\n\tfmt.Fprintf(os.Stderr, \"%s\\n\", err)\n\tos.Exit(1)\n}\n\n// dumpMakeDb dumps the internal make(1) database and returns it\nfunc dumpMakeDb(file string) (io.Reader, error) {\n\tdir := path.Clean(path.Dir(file))\n\targs := []string{\n\t\t\"--makefile\",\n\t\tfile,\n\t\t\"--directory\",\n\t\tdir,\n\t\t\"--print-data-base\",\n\t\t\"--no-builtin-rules\",\n\t\t\"--no-builtin-variables\",\n\t\t\"--dry-run\",\n\t\t\"--always-make\",\n\t\t\"--question\",\n\t}\n\n\t// Pass in the calling process environment, which might be needed when\n\t// evaluating dynamic targets coming from Makefile variables calling out\n\t// to shell. Also, sanitize the environment from LC_* vars and set\n\t// LC_ALL=C, so that we have deterministic output of the internal\n\t// database.\n\tenv := os.Environ()\n\tsanitizedEnv := slices.DeleteFunc(env, func(item string) bool {\n\t\treturn strings.HasPrefix(item, \"LC_\")\n\t})\n\tsanitizedEnv = append(sanitizedEnv, \"LC_ALL=C\")\n\n\tcmd := exec.Command(\"make\", args...)\n\tcmd.Env = sanitizedEnv\n\tcmd.Dir = dir\n\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tif exiterr, ok := err.(*exec.ExitError); ok {\n\t\t\t// We ignore exit code 1 and 2 here. Exit code 1 will be\n\t\t\t// returned when a target is not up-to-date and usually\n\t\t\t// exit code 2 is returned when a pre-requisite file is\n\t\t\t// missing.  In both cases we can ignore the exit codes,\n\t\t\t// since we are interested in dumping the internal db\n\t\t\t// only.\n\t\t\texitCode := exiterr.ExitCode()\n\t\t\tif exitCode != 1 && exitCode != 2 {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\t// Some other error occurred, bubble it up\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tr := bytes.NewReader(output)\n\n\treturn r, nil\n}\n\n// writeEchartsTree generates a tree of the Makefile targets using echarts.\nfunc writeEchartsTree(g graph.Graph[string], w io.Writer, globalOpts []charts.GlobalOpts, seriesOpts []charts.SeriesOpts) error {\n\t// Build a map of the tree nodes and use it later for building the tree.\n\tnodesMap := make(map[string]*opts.TreeData)\n\tfor _, u := range g.GetVertices() {\n\t\tnode := &opts.TreeData{\n\t\t\tName:       u.Label,\n\t\t\tSymbolSize: 15,\n\t\t\tItemStyle:  u.EchartsStyle,\n\t\t}\n\t\tnodesMap[u.Label] = node\n\t}\n\n\t// Topowalk the graph and build the tree data\n\ttreeData := make([]*opts.TreeData, 0)\n\twalker := func(u *graph.Vertex[string]) error {\n\t\tnode := nodesMap[u.Label]\n\t\tchildren := make([]*opts.TreeData, 0)\n\t\tfor _, v := range g.GetNeighbourVertices(u.Value) {\n\t\t\tchild := nodesMap[v.Label]\n\t\t\tchildren = append(children, child)\n\t\t}\n\t\tnode.Children = children\n\n\t\t// Add only top-level targets to the tree. Children nodes will\n\t\t// already be attached to their respective parents.\n\t\tif u.Degree.In == 0 {\n\t\t\ttreeData = append(treeData, node)\n\t\t}\n\n\t\treturn nil\n\t}\n\tif err := graph.WalkTopoOrder(g, walker); err != nil {\n\t\treturn err\n\t}\n\n\t// Attach nodes to a root node.\n\troot := opts.TreeData{\n\t\tName:     \"Root\",\n\t\tChildren: treeData,\n\t}\n\n\ttree := charts.NewTree()\n\ttree.SetGlobalOptions(globalOpts...)\n\ttree.AddSeries(\"Targets\", []opts.TreeData{root}).SetSeriesOptions(seriesOpts...)\n\ttree.AddJSFuncStrs(`%MY_ECHARTS%.setOption({\"emphasis\": {\"focus\": \"descendant\"}});`)\n\tpage := components.NewPage()\n\tpage.SetPageTitle(\"makefile-graph\")\n\tpage.AddCharts(tree)\n\n\treturn page.Render(w)\n}\n"
  },
  {
    "path": "examples/Makefile",
    "content": "# Example Makefile from [1].\n#\n# [1]: https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html\nedit : main.o kbd.o command.o display.o \\\n       insert.o search.o files.o utils.o\n\tcc -o edit main.o kbd.o command.o display.o \\\n\t\t   insert.o search.o files.o utils.o\n\nmain.o : main.c defs.h\n\tcc -c main.c\nkbd.o : kbd.c defs.h command.h\n\tcc -c kbd.c\ncommand.o : command.c defs.h command.h\n\tcc -c command.c\ndisplay.o : display.c defs.h buffer.h\n\tcc -c display.c\ninsert.o : insert.c defs.h buffer.h\n\tcc -c insert.c\nsearch.o : search.c defs.h buffer.h\n\tcc -c search.c\nfiles.o : files.c defs.h buffer.h command.h\n\tcc -c files.c\nutils.o : utils.c defs.h\n\tcc -c utils.c\nclean :\n\trm edit main.o kbd.o command.o display.o \\\n\t   insert.o search.o files.o utils.o\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dnaeon/makefile-graph\n\ngo 1.23\n\nrequire gopkg.in/dnaeon/go-graph.v1 v1.0.2\n\nrequire (\n\tgithub.com/go-echarts/go-echarts/v2 v2.5.4 // indirect\n\tgopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 // indirect\n\tgopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-echarts/go-echarts/v2 v2.5.4 h1:bw0REczgtgI/o7GPqae4AzsiJwwyJvyWwJ7vuM0G6tQ=\ngithub.com/go-echarts/go-echarts/v2 v2.5.4/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=\ngithub.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 h1:wB8d7G8z0rrFgS/Wr16pBVL3MxFp8M+/fTJGcBAxn1k=\ngopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755/go.mod h1:mAZ2p0oPgDAeRlUKq3IJOE9RvqVvA3BIA4WKxG/9zDM=\ngopkg.in/dnaeon/go-graph.v1 v1.0.2 h1:OHShBkXqG65QSQwU0/CQfcqMvJh/+XoR7gZiPprtEVc=\ngopkg.in/dnaeon/go-graph.v1 v1.0.2/go.mod h1:N9qN2W5CwdzCVmrqtLPmQ3HawuV81HBhuIyUJiMoXT0=\ngopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 h1:AS4IIyStH/47cZpPdBk/Db6QXg+UmcDjZvSOoDgGsbU=\ngopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1/go.mod h1:JNUtwj2QQsBHhsIHNjxdDaSmLW4RZtvJHO8VYtsMkY4=\ngopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=\ngopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "pkg/fixtures/fixtures.go",
    "content": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions\n// are met:\n//\n//   1. Redistributions of source code must retain the above copyright\n//      notice, this list of conditions and the following disclaimer.\n//   2. Redistributions in binary form must reproduce the above copyright\n//      notice, this list of conditions and the following disclaimer in the\n//      documentation and/or other materials provided with the distribution.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n// POSSIBILITY OF SUCH DAMAGE.\n\npackage fixtures\n\nimport _ \"embed\"\n\n//go:embed sample_gnu_make_db_3.81.txt\nvar SampleDb_v3_81 string\n\n//go:embed sample_gnu_make_db_4.4.1.txt\nvar SampleDb_v4_4_1 string\n"
  },
  {
    "path": "pkg/fixtures/sample_gnu_make_db_3.81.txt",
    "content": "# GNU Make 3.81\n# Copyright (C) 2006  Free Software Foundation, Inc.\n# This is free software; see the source for copying conditions.\n# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n# PARTICULAR PURPOSE.\n\n# This program built for i386-apple-darwin11.3.0\nmkdir -p /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\ngo build -o /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph cmd/main.go\n\n# Make data base, printed on Fri Apr 20 15:08:33 2024\n\n# Variables\n\n# automatic\n<D = $(patsubst %/,%,$(dir $<))\n# automatic\n?F = $(notdir $?)\n# environment\nLC_CTYPE = UTF-8\n# default\nCWEAVE = cweave\n# automatic\n?D = $(patsubst %/,%,$(dir $?))\n# automatic\n@D = $(patsubst %/,%,$(dir $@))\n# automatic\n@F = $(notdir $@)\n# default\nPC = pc\n# default\nMAKE_VERSION := 3.81\n# environment\nEDITOR = emacsclient\n# default\nFC = f77\n# makefile (from `Makefile', line 1)\n.DEFAULT_GOAL := build\n# automatic\n%D = $(patsubst %/,%,$(dir $%))\n# default\nWEAVE = weave\n# default\nLINK.cpp = $(LINK.cc)\n# default\nF77 = $(FC)\n# default\n.VARIABLES := \n# automatic\n*F = $(notdir $*)\n# default\nCOMPILE.def = $(M2C) $(M2FLAGS) $(DEFFLAGS) $(TARGET_ARCH)\n# default\nLEX = lex\n# makefile\nMAKEFLAGS = rpnB\n# environment\nMFLAGS = -rpnB\n# automatic\n*D = $(patsubst %/,%,$(dir $*))\n# default\nLEX.l = $(LEX) $(LFLAGS) -t\n# environment\nXPC_SERVICE_NAME = 0\n# environment\nLC_TERMINAL_VERSION = 3.4.23\n# automatic\n+D = $(patsubst %/,%,$(dir $+))\n# default\nCOMPILE.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -c\n# automatic\n+F = $(notdir $+)\n# default\nGNUMAKE = YES\n# variable set hash-table stats:\n# Load=138/1024=13%, Rehash=0, Collisions=13/187=7%\n\n# Pattern-specific Variable Values\n\n# No pattern-specific variable values.\n\n# Directories\n\n# . (device 16777233, inode 7066412): 22 files, no impossibilities.\n\n# 22 files, no impossibilities in 1 directories.\n\n# Implicit Rules\n\n# No implicit rules.\n\n# Files\n\ntest-cover:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  commands to execute (from `Makefile', line 20):\n\tgo test -v -race -coverprofile=coverage.txt -covermode=atomic ./...\n\t\n\n.PHONY: get test test-cover build\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\n# Not a target:\n.SUFFIXES:\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\n# Not a target:\nMakefile:\n#  Implicit rule search has been done.\n#  Last modified 2024-04-19 14:02:58\n#  File has been updated.\n#  Successfully updated.\n# variable set hash-table stats:\n# Load=0/32=0%, Rehash=0, Collisions=0/0=0%\n\ntest:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  commands to execute (from `Makefile', line 17):\n\tgo test -v -race ./...\n\t\n\nbuild: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has been updated.\n#  Successfully updated.\n# variable set hash-table stats:\n# Load=0/32=0%, Rehash=0, Collisions=0/11=0%\n\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n#  Implicit rule search has not been done.\n#  Implicit/static pattern stem: `'\n#  Last modified 1970-01-01 01:59:56\n#  File has been updated.\n#  Successfully updated.\n# automatic\n# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph\n# automatic\n# % := \n# automatic\n# * := \n# automatic\n# + := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# | := \n# automatic\n# < := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# ^ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# ? := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# variable set hash-table stats:\n# Load=8/32=25%, Rehash=0, Collisions=2/19=11%\n#  commands to execute (from `Makefile', line 9):\n\tgo build -o $(BINARY) cmd/main.go\n\t\n\n# Not a target:\n.DEFAULT:\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin:\n#  Implicit rule search has not been done.\n#  Implicit/static pattern stem: `'\n#  Last modified 1970-01-01 01:59:56\n#  File has been updated.\n#  Successfully updated.\n# automatic\n# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# % := \n# automatic\n# * := \n# automatic\n# + := \n# automatic\n# | := \n# automatic\n# < := \n# automatic\n# ^ := \n# automatic\n# ? := \n# variable set hash-table stats:\n# Load=8/32=25%, Rehash=0, Collisions=1/13=8%\n#  commands to execute (from `Makefile', line 6):\n\tmkdir -p $(LOCAL_BIN)\n\t\n\nget:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  commands to execute (from `Makefile', line 14):\n\tgo get -v -t -d ./...\n\t\n\n# files hash-table stats:\n# Load=10/1024=1%, Rehash=0, Collisions=0/30=0%\n# VPATH Search Paths\n\n# No `vpath' search paths.\n\n# No general (`VPATH' variable) search path.\n\n# # of strings in strcache: 1\n# # of strcache buffers: 1\n# strcache size: total = 4096 / max = 4096 / min = 4096 / avg = 4096\n# strcache free: total = 4087 / max = 4087 / min = 4087 / avg = 4087\n\n# Finished Make data base on Fri Apr 20 15:08:33 2024\n\n"
  },
  {
    "path": "pkg/fixtures/sample_gnu_make_db_4.4.1.txt",
    "content": "mkdir -p /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\ngo build -o /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph cmd/main.go\n# GNU Make 4.4.1\n# Built for aarch64-apple-darwin23.0.0\n# Copyright (C) 1988-2023 Free Software Foundation, Inc.\n# License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>\n# This is free software: you are free to change and redistribute it.\n# There is NO WARRANTY, to the extent permitted by law.\n\n# Make data base, printed on Sat Apr 20 14:59:39 2024\n\n# Variables\n\n# default\nPREPROCESS.S = $(CPP) $(CPPFLAGS)\n# environment\nGO111MODULE = on\n# default\nCOMPILE.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c\n# default\nARFLAGS = -rv\n# default\nAS = as\n# default\nAR = ar\n# default\nOBJC = cc\n# environment\nXPC_SERVICE_NAME = 0\n# default\nLINK.S = $(CC) $(ASFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_MACH)\n# default\nLINK.s = $(CC) $(ASFLAGS) $(LDFLAGS) $(TARGET_MACH)\n# default\nMAKE_COMMAND := gmake\n# automatic\n@D = $(patsubst %/,%,$(dir $@))\n# default\nCOFLAGS = \n# default\nCOMPILE.mod = $(M2C) $(M2FLAGS) $(MODFLAGS) $(TARGET_ARCH)\n# makefile (from 'Makefile', line 3)\nBINARY = $(LOCAL_BIN)/makefile-graph\n# default\n.VARIABLES := \n# environment\nCOMMAND_MODE = unix2003\n# automatic\n%D = $(patsubst %/,%,$(dir $%))\n# default\nLINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)\n# default\nTEXI2DVI = texi2dvi\n# automatic\n^D = $(patsubst %/,%,$(dir $^))\n# automatic\n%F = $(notdir $%)\n# default\nLEX.l = $(LEX) $(LFLAGS) -t\n# environment\nLANG = en_US.UTF-8\n# default\n.LOADED := \n# environment\nCOLORFGBG = 12;8\n# environment\n__CF_USER_TEXT_ENCODING = 0x0:0:0\n# default\nCOMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c\n# makefile\nMAKEFLAGS = Bnpr\n# default\nLINK.f = $(FC) $(FFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# default\nTANGLE = tangle\n# default\nPREPROCESS.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -F\n# automatic\n*D = $(patsubst %/,%,$(dir $*))\n# environment\nMFLAGS = -Bnpr\n# default\n.SHELLFLAGS := -c\n# default\nM2C = m2c\n# default\nCOMPILE.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c\n# default\nCOMPILE.cpp = $(COMPILE.cc)\n# default\nTEX = tex\n# automatic\n+D = $(patsubst %/,%,$(dir $+))\n# makefile (from 'Makefile', line 1)\nMAKEFILE_LIST := Makefile\n# default\nF77FLAGS = $(FFLAGS)\n# automatic\n@F = $(notdir $@)\n# automatic\n?D = $(patsubst %/,%,$(dir $?))\n# default\nCOMPILE.def = $(M2C) $(M2FLAGS) $(DEFFLAGS) $(TARGET_ARCH)\n# default\nCTANGLE = ctangle\n# automatic\n*F = $(notdir $*)\n# automatic\n<D = $(patsubst %/,%,$(dir $<))\n# environment\nITERM_PROFILE = Default\n# default\nCOMPILE.C = $(COMPILE.cc)\n# default\nYACC.m = $(YACC) $(YFLAGS)\n# default\nLINK.C = $(LINK.cc)\n# default\nMAKE_HOST := aarch64-apple-darwin23.0.0\n# default\nLINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# makefile\nSHELL = /bin/sh\n# default\nLINK.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# environment\nSHLVL = 2\n# environment\nMAKELEVEL := 0\n# default\nMAKE = $(MAKE_COMMAND)\n# default\nFC = f77\n# default\nLINT = lint\n# default\nPC = pc\n# default\nMAKEFILES := \n# automatic\n^F = $(notdir $^)\n# default\nLEX.m = $(LEX) $(LFLAGS) -t\n# default\n.LIBPATTERNS = lib%.dylib lib%.a\n# default\nCPP = $(CC) -E\n# default\nLINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# default\nCHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)\n# default\nCOMPILE.f = $(FC) $(FFLAGS) $(TARGET_ARCH) -c\n# default\nCOMPILE.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -c\n# default\nCOMPILE.S = $(CC) $(ASFLAGS) $(CPPFLAGS) $(TARGET_MACH) -c\n# automatic\n?F = $(notdir $?)\n# default\nGET = get\n# default\nLINK.r = $(FC) $(FFLAGS) $(RFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# automatic\n+F = $(notdir $+)\n# default\nMAKEINFO = makeinfo\n# 'override' directive\nGNUMAKEFLAGS := \n# default\nPREPROCESS.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -F\n# default\nLINK.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# environment\nLOGNAME = dnaeon\n# default\nLINK.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)\n# default\nYACC = yacc\n# environment\nXPC_FLAGS = 0x0\n# makefile (from 'Makefile', line 1)\n.DEFAULT_GOAL := build\n# default\nRM = rm -f\n# environment\nSAVEHIST = 1000\n# default\nWEAVE = weave\n# environment\nEDITOR = emacsclient\n# environment\nUSER = dnaeon\n# default\nMAKE_VERSION := 4.4.1\n# 'override' directive\n.SHELLSTATUS := 0\n# default\nF77 = $(FC)\n# default\nCWEAVE = cweave\n# default\nYACC.y = $(YACC) $(YFLAGS)\n# default\nLINK.cpp = $(LINK.cc)\n# default\nCO = co\n# environment\nCOLORTERM = truecolor\n# environment\nLC_CTYPE = UTF-8\n# default\nOUTPUT_OPTION = -o $@\n# default\nCOMPILE.s = $(AS) $(ASFLAGS) $(TARGET_MACH)\n# default\nMAKE_TERMERR := /dev/ttys002\n# environment\nHOME = /Users/dnaeon\n# default\nLEX = lex\n# environment\nTERM = tmux-256color\n# default\nLINT.c = $(LINT) $(LINTFLAGS) $(CPPFLAGS) $(TARGET_ARCH)\n# default\nCOMPILE.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c\n# default\n.RECIPEPREFIX := \n# automatic\n<F = $(notdir $<)\n# default\nSUFFIXES := \n# default\nLD = ld\n# default\n.FEATURES := target-specific order-only second-expansion else-if shortest-stem undefine oneshell nocomment grouped-target extra-prereqs notintermediate shell-export archives jobserver jobserver-fifo output-sync check-symlink load\n# default\nCXX = clang++\n# default\nCC = cc\n# makefile (from 'Makefile', line 2)\nLOCAL_BIN = $(shell pwd)/bin\n# default\nCOMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c\n# variable set hash-table stats:\n# Load=145/1024=14%, Rehash=0, Collisions=14/211=7%\n\n# Pattern-specific Variable Values\n\n# No pattern-specific variable values.\n\n# Directories\n\n# . (device 16777231, inode 7066412): 14 files, no impossibilities.\n\n# 14 files, no impossibilities in 1 directories.\n\n# Implicit Rules\n\n# No implicit rules.\n\n# Files\n\n# 'override' directive\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin: .SHELLSTATUS := 0\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin:\n#  Implicit rule search has not been done.\n#  Implicit/static pattern stem: ''\n#  Last modified 2514-05-30 04:53:03.107374182\n#  File has been updated.\n#  Successfully updated.\n# automatic\n# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# * := \n# automatic\n# < := \n# automatic\n# + := \n# automatic\n# % := \n# automatic\n# ^ := \n# automatic\n# ? := \n# automatic\n# | := \n# variable set hash-table stats:\n# Load=9/32=28%, Rehash=0, Collisions=1/16=6%\n#  recipe to execute (from 'Makefile', line 6):\n\tmkdir -p $(LOCAL_BIN)\n\n# Not a target:\nMakefile:\n#  Implicit rule search has been done.\n#  File is secondary (prerequisite of .SECONDARY).\n#  Last modified 2024-04-19 15:25:48.106252418\n#  File has been updated.\n#  Successfully updated.\n\ntest-cover:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  recipe to execute (from 'Makefile', line 20):\n\tgo test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -v cmd)\n\n# 'override' directive\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: .SHELLSTATUS := 0\n/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n#  Implicit rule search has not been done.\n#  Implicit/static pattern stem: ''\n#  Last modified 2514-05-30 04:53:03.107374182\n#  File has been updated.\n#  Successfully updated.\n# automatic\n# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph\n# automatic\n# * := \n# automatic\n# < := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# + := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# % := \n# automatic\n# ^ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# ? := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\n# automatic\n# | := \n# variable set hash-table stats:\n# Load=9/32=28%, Rehash=0, Collisions=1/24=4%\n#  recipe to execute (from 'Makefile', line 9):\n\tgo build -o $(BINARY) cmd/main.go\n\n# Not a target:\n.DEFAULT:\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\nbuild: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has been updated.\n#  Successfully updated.\n# variable set hash-table stats:\n# Load=0/32=0%, Rehash=0, Collisions=0/15=0%\n\ntest:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  recipe to execute (from 'Makefile', line 17):\n\tgo test -v -race $(shell go list ./... | grep -v cmd)\n\nget:\n#  Phony target (prerequisite of .PHONY).\n#  Implicit rule search has not been done.\n#  File does not exist.\n#  File has not been updated.\n#  recipe to execute (from 'Makefile', line 14):\n\tgo get -v -t -d ./...\n\n# Not a target:\n.SUFFIXES:\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\n.PHONY: get test test-cover build\n#  Implicit rule search has not been done.\n#  Modification time never checked.\n#  File has not been updated.\n\n# files hash-table stats:\n# Load=10/1024=1%, Rehash=0, Collisions=0/30=0%\n# VPATH Search Paths\n\n# No 'vpath' search paths.\n\n# No general ('VPATH' variable) search path.\n\n# strcache buffers: 1 (0) / strings = 24 / storage = 362 B / avg = 15 B\n# current buf: size = 8162 B / used = 362 B / count = 24 / avg = 15 B\n\n# strcache performance: lookups = 41 / hit rate = 41%\n# hash-table stats:\n# Load=24/8192=0%, Rehash=0, Collisions=0/41=0%\n# Finished Make data base on Sat Apr 20 14:59:39 2024\n\n"
  },
  {
    "path": "pkg/parser/parser.go",
    "content": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions\n// are met:\n//\n//   1. Redistributions of source code must retain the above copyright\n//      notice, this list of conditions and the following disclaimer.\n//   2. Redistributions in binary form must reproduce the above copyright\n//      notice, this list of conditions and the following disclaimer in the\n//      documentation and/or other materials provided with the distribution.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n// POSSIBILITY OF SUCH DAMAGE.\n\npackage parser\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"gopkg.in/dnaeon/go-graph.v1\"\n)\n\n// ErrInvalidTarget is an error returned by the [Parser] when trying to parse an\n// invalid target.\nvar ErrInvalidTarget = errors.New(\"invalid target\")\n\nconst (\n\t// Name of the .PHONY target\n\tphonyTarget = \".PHONY\"\n\n\t// Marker specifying the beginning of Files section\n\tfilesSectionMarker = \"# Files\"\n)\n\n// Parser is a naive parser which knows how to parse the internal GNU Make\n// database.\ntype Parser struct{}\n\n// New creates a new [Parser]\nfunc New() *Parser {\n\tp := &Parser{}\n\n\treturn p\n}\n\n// readLine reads a single line from the given reader and returns it as a string\nfunc (p *Parser) readLine(r *bufio.Reader) (string, error) {\n\tvar b strings.Builder\n\tfor {\n\t\tline, isPrefix, err := r.ReadLine()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tb.Write(line)\n\t\tif !isPrefix {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn b.String(), nil\n}\n\n// Parse parses the data from the given [io.Reader] line by line and generates a\n// dependency graph for the discovered targets.\nfunc (p *Parser) Parse(r io.Reader) (graph.Graph[string], error) {\n\tg := graph.New[string](graph.KindDirected)\n\tscanner := bufio.NewReader(r)\n\n\t// Navigate to the `Files` section\n\tfor {\n\t\tline, err := p.readLine(scanner)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif line == filesSectionMarker {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Parse the targets\nL:\n\tfor {\n\t\tline, err := p.readLine(scanner)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak L\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch {\n\t\tcase line == \"# Not a target:\":\n\t\t\t// Skip next line\n\t\t\t_, err := p.readLine(scanner)\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak L\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontinue\n\t\tcase strings.Contains(line, \" = \"), strings.Contains(line, \" := \"):\n\t\t\t// Variables\n\t\t\tcontinue\n\t\tcase strings.HasPrefix(line, \"#\"):\n\t\t\t// Comment\n\t\t\tcontinue\n\t\tcase strings.HasPrefix(line, \"\\t\"):\n\t\t\t// Recipe\n\t\t\tcontinue\n\t\tcase strings.Contains(line, \":\"):\n\t\t\t// Target\n\t\t\tif err := p.parseVertices(g, line); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn g, nil\n}\n\n// parseVertices parses a single line, which represents a GNU Make target with\n// it's prerequisites.\nfunc (p *Parser) parseVertices(g graph.Graph[string], line string) error {\n\t// A typical target with pre-requisites looks like this\n\t//\n\t// target-name: pre-req-1 pre-req-2 ...\n\titems := strings.Split(line, \":\")\n\tif len(items) < 2 {\n\t\treturn fmt.Errorf(\"%w: %s\", ErrInvalidTarget, line)\n\t}\n\n\t// Add the vertices and edges\n\tfromItems := items[0]\n\ttoItems := items[1]\n\tfor _, u := range strings.Split(fromItems, \" \") {\n\t\tu = strings.TrimSpace(u)\n\t\tif u == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif u != phonyTarget {\n\t\t\tg.AddVertex(u)\n\t\t}\n\n\t\tfor _, v := range strings.Split(toItems, \" \") {\n\t\t\tv = strings.TrimSpace(v)\n\t\t\tif v == \"\" || v == \"|\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tg.AddVertex(v)\n\t\t\tif u != phonyTarget {\n\t\t\t\tg.AddEdge(u, v)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/parser/parser_test.go",
    "content": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions\n// are met:\n//\n//   1. Redistributions of source code must retain the above copyright\n//      notice, this list of conditions and the following disclaimer.\n//   2. Redistributions in binary form must reproduce the above copyright\n//      notice, this list of conditions and the following disclaimer in the\n//      documentation and/or other materials provided with the distribution.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n// POSSIBILITY OF SUCH DAMAGE.\n\npackage parser\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/dnaeon/makefile-graph/pkg/fixtures\"\n\t\"gopkg.in/dnaeon/go-graph.v1\"\n)\n\nfunc TestWithSampleDatabases(t *testing.T) {\n\ttype testCase struct {\n\t\tdesc    string\n\t\tdb      string\n\t\twantVs  int\n\t\twantEs  int\n\t\twantErr error\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tdesc:    \"GNU Make 3.81 database\",\n\t\t\tdb:      fixtures.SampleDb_v3_81,\n\t\t\twantVs:  6,\n\t\t\twantEs:  2,\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tdesc:    \"GNU Make 4.4.1 database\",\n\t\t\tdb:      fixtures.SampleDb_v4_4_1,\n\t\t\twantVs:  6,\n\t\t\twantEs:  2,\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\tp := New()\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.desc, func(t *testing.T) {\n\t\t\tr := strings.NewReader(tc.db)\n\t\t\tg, err := p.Parse(r)\n\t\t\tif !errors.Is(err, tc.wantErr) {\n\t\t\t\tt.Fatalf(\"got unexpected error: %s\", err)\n\t\t\t}\n\n\t\t\tgotVs := len(g.GetVertices())\n\t\t\tgotEs := len(g.GetEdges())\n\t\t\tif tc.wantVs != gotVs {\n\t\t\t\tt.Errorf(\"want |V|=%d, got |V|=%d\", tc.wantVs, gotVs)\n\t\t\t}\n\n\t\t\tif tc.wantEs != gotEs {\n\t\t\t\tt.Errorf(\"want |E|=%d, got |E|=%d\", tc.wantEs, gotEs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseVerticesLine(t *testing.T) {\n\ttype testCase struct {\n\t\t// A line representing a target\n\t\tline string\n\t\t// expected error\n\t\twantErr error\n\t\t// number of expected vertices\n\t\twantVs int\n\t\t// number of expected edges\n\t\twantEs int\n\t}\n\n\ttestCases := []testCase{\n\t\t{line: \"foo:\", wantErr: nil, wantVs: 1, wantEs: 0},\n\t\t{line: \"foo: bar\", wantErr: nil, wantVs: 2, wantEs: 1},\n\t\t{line: \"foo: | bar\", wantErr: nil, wantVs: 2, wantEs: 1},\n\t\t{line: \"foo: bar | baz\", wantErr: nil, wantVs: 3, wantEs: 2},\n\t\t{line: \"foo: baz qux\", wantErr: nil, wantVs: 3, wantEs: 2},\n\t\t{line: \"foo bar: baz qux\", wantErr: nil, wantVs: 4, wantEs: 4},\n\t\t{line: \"foo\", wantErr: ErrInvalidTarget},\n\t}\n\n\tp := New()\n\tfor _, tc := range testCases {\n\t\tname := fmt.Sprintf(\"line(%s), |V|=%d, |E|=%d\", tc.line, tc.wantVs, tc.wantEs)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tg := graph.New[string](graph.KindDirected)\n\t\t\terr := p.parseVertices(g, tc.line)\n\t\t\tif !errors.Is(err, tc.wantErr) {\n\t\t\t\tt.Errorf(\"want err %s, got err %s\", tc.wantErr, err)\n\t\t\t}\n\t\t\tgotVs := len(g.GetVertices())\n\t\t\tgotEs := len(g.GetEdges())\n\n\t\t\tif gotVs != tc.wantVs {\n\t\t\t\tt.Errorf(\"want |V|=%d, got |V|=%d\", tc.wantVs, gotVs)\n\t\t\t}\n\n\t\t\tif gotEs != tc.wantEs {\n\t\t\t\tt.Errorf(\"want |E|=%d, got |E|=%d\", tc.wantEs, gotEs)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\"\n  ],\n    \"postUpdateOptions\": [\n    \"gomodTidy\",\n  ]\n}\n"
  }
]