├── tests ├── fixtures │ ├── path_in_assignments │ │ ├── input.mk │ │ └── expected.mk │ ├── urls_in_assignments │ │ ├── expected.mk │ │ └── input.mk │ ├── urls_in_assignments_datetime │ │ ├── expected.mk │ │ └── input.mk │ ├── urls_in_assignments_quoted │ │ ├── expected.mk │ │ └── input.mk │ ├── multiline_assignment_with_url │ │ ├── input.mk │ │ └── expected.mk │ ├── substitution_reference_guard │ │ ├── input.mk │ │ └── expected.mk │ ├── recipeprefix_urls │ │ ├── input.mk │ │ └── expected.mk │ ├── colon_sensitive_assignment │ │ ├── input.mk │ │ └── expected.mk │ ├── urls_in_recipes │ │ ├── input.mk │ │ └── expected.mk │ ├── urls_in_recipes_quoted │ │ ├── expected.mk │ │ └── input.mk │ ├── urls_in_recipes_datetime │ │ ├── input.mk │ │ └── expected.mk │ ├── conditional_urls │ │ ├── expected.mk │ │ └── input.mk │ ├── vpath_variations │ │ ├── expected.mk │ │ └── input.mk │ ├── recipeprefix_multi │ │ ├── input.mk │ │ └── expected.mk │ ├── suffix_phony_issue │ │ ├── input.mk │ │ └── expected.mk │ ├── whitespace_normalization │ │ ├── expected.mk │ │ └── input.mk │ ├── define_endef │ │ ├── input.mk │ │ └── expected.mk │ ├── phony_targets │ │ ├── input.mk │ │ └── expected.mk │ ├── pattern_rules │ │ ├── input.mk │ │ ├── expected.mk │ │ └── input_backup.mk │ ├── shell_formatting │ │ ├── input.mk │ │ └── expected.mk │ ├── makefile_vars_in_shell │ │ ├── input.mk │ │ └── expected.mk │ ├── vpath_conditionals │ │ ├── expected.mk │ │ └── input.mk │ ├── vpath_edge_cases │ │ ├── expected.mk │ │ └── input.mk │ ├── suffix_rules │ │ ├── input.mk │ │ └── expected.mk │ ├── target_spacing │ │ ├── expected.mk │ │ ├── input.mk │ │ └── input_backup.mk │ ├── vpath_advanced │ │ ├── input.mk │ │ └── expected.mk │ ├── line_continuations │ │ ├── expected.mk │ │ └── input.mk │ ├── nested_conditional_alignment │ │ ├── input.mk │ │ └── expected.mk │ ├── recipe_tabs │ │ ├── expected.mk │ │ └── input.mk │ ├── complex │ │ ├── expected.mk │ │ └── input.mk │ ├── variable_assignments │ │ ├── input.mk │ │ └── expected.mk │ ├── invalid_targets │ │ ├── input.mk │ │ └── expected.mk │ ├── nested_conditional_indentation │ │ ├── expected.mk │ │ └── input.mk │ ├── expected.mk │ ├── input.mk │ ├── duplicate_targets_conditional │ │ ├── input.mk │ │ └── expected.mk │ ├── format_disable │ │ ├── input.mk │ │ └── expected.mk │ ├── special_targets │ │ ├── input.mk │ │ └── expected.mk │ ├── backslash_continuation_block │ │ ├── expected.mk │ │ └── input.mk │ ├── comment_only_targets │ │ ├── input.mk │ │ └── expected.mk │ ├── conditional_blocks │ │ ├── expected.mk │ │ └── input.mk │ ├── comments_and_documentation │ │ ├── expected.mk │ │ └── input.mk │ ├── error_handling │ │ ├── input.mk │ │ └── expected.mk │ ├── includes_and_exports │ │ ├── input.mk │ │ └── expected.mk │ ├── function_calls │ │ ├── input.mk │ │ └── expected.mk │ ├── shell_operators │ │ ├── input.mk │ │ └── expected.mk │ ├── complex_conditionals │ │ ├── expected.mk │ │ └── input.mk │ ├── variable_references │ │ ├── expected.mk │ │ └── input.mk │ ├── unicode_and_encoding │ │ ├── input.mk │ │ └── expected.mk │ ├── multiline_variables │ │ ├── input.mk │ │ └── expected.mk │ ├── advanced_targets │ │ ├── expected.mk │ │ └── input.mk │ ├── edge_cases_and_quirks │ │ ├── expected.mk │ │ └── input.mk │ └── real_world_complex │ │ ├── input.mk │ │ └── expected.mk ├── verilator │ ├── Makefile │ ├── Makefile.txt │ ├── Makefile-2.in │ └── Makefile-3.txt ├── conftest.py └── test_validate_command.py ├── demo.mk ├── Makefile ├── vscode-mbake-extension ├── icon.png ├── LICENSE └── package.json ├── mbake ├── core │ ├── __init__.py │ └── rules │ │ ├── __init__.py │ │ ├── whitespace.py │ │ ├── rule_type_detection.py │ │ ├── final_newline.py │ │ ├── target_validation.py │ │ ├── pattern_spacing.py │ │ ├── special_target_validation.py │ │ ├── assignment_spacing.py │ │ ├── duplicate_targets.py │ │ ├── suffix_validation.py │ │ └── shell.py ├── __main__.py ├── __init__.py ├── constants │ ├── __init__.py │ ├── phony_targets.py │ ├── makefile_targets.py │ └── shell_commands.py ├── utils │ └── __init__.py └── plugins │ └── base.py ├── MANIFEST.in ├── .pre-commit-hooks.yaml ├── .bake.toml.example ├── LICENSE ├── completions ├── bash │ └── mbake ├── zsh │ └── _mbake └── fish │ └── mbake.fish ├── .github └── workflows │ └── test.yml ├── .gitignore ├── pyproject.toml ├── CONTRIBUTING.md └── README.md /tests/fixtures/path_in_assignments/input.mk: -------------------------------------------------------------------------------- 1 | WIN_PATH = C:\\Program Files\\App 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments/expected.mk: -------------------------------------------------------------------------------- 1 | VARIABLE = http://www.github.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments/input.mk: -------------------------------------------------------------------------------- 1 | VARIABLE = http://www.github.com 2 | -------------------------------------------------------------------------------- /demo.mk: -------------------------------------------------------------------------------- 1 | # Quick test 2 | # make -f demo.mk all 3 | CC = gcc 4 | all: 5 | echo test 6 | -------------------------------------------------------------------------------- /tests/fixtures/path_in_assignments/expected.mk: -------------------------------------------------------------------------------- 1 | WIN_PATH = C:\\Program Files\\App 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments_datetime/expected.mk: -------------------------------------------------------------------------------- 1 | BUILD_TIME = 2025-10-13T12:34:56Z 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments_datetime/input.mk: -------------------------------------------------------------------------------- 1 | BUILD_TIME = 2025-10-13T12:34:56Z 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments_quoted/expected.mk: -------------------------------------------------------------------------------- 1 | VARIABLE = "http://www.github.com" 2 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_assignments_quoted/input.mk: -------------------------------------------------------------------------------- 1 | VARIABLE = "http://www.github.com" 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | 3 | lint: 4 | black mbake 5 | ruff check mbake 6 | mypy mbake 7 | -------------------------------------------------------------------------------- /tests/fixtures/multiline_assignment_with_url/input.mk: -------------------------------------------------------------------------------- 1 | BASE = http://example.com \ 2 | /path 3 | -------------------------------------------------------------------------------- /tests/fixtures/multiline_assignment_with_url/expected.mk: -------------------------------------------------------------------------------- 1 | BASE = http://example.com \ 2 | /path 3 | -------------------------------------------------------------------------------- /tests/fixtures/substitution_reference_guard/input.mk: -------------------------------------------------------------------------------- 1 | SRC := a.c b.c 2 | OBJ := $(SRC:.c=.o) 3 | 4 | all: $(OBJ) 5 | -------------------------------------------------------------------------------- /vscode-mbake-extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbodShojaei/bake/HEAD/vscode-mbake-extension/icon.png -------------------------------------------------------------------------------- /tests/fixtures/recipeprefix_urls/input.mk: -------------------------------------------------------------------------------- 1 | .RECIPEPREFIX := > 2 | one: 3 | >SOME_URL=http://github.com \ 4 | > echo ok 5 | -------------------------------------------------------------------------------- /tests/fixtures/substitution_reference_guard/expected.mk: -------------------------------------------------------------------------------- 1 | SRC := a.c b.c 2 | OBJ := $(SRC:.c=.o) 3 | 4 | all: $(OBJ) 5 | -------------------------------------------------------------------------------- /tests/fixtures/recipeprefix_urls/expected.mk: -------------------------------------------------------------------------------- 1 | .RECIPEPREFIX := > 2 | one: 3 | >SOME_URL=http://github.com \ 4 | > echo ok 5 | -------------------------------------------------------------------------------- /mbake/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Core formatting functionality.""" 2 | 3 | from .formatter import MakefileFormatter 4 | 5 | __all__ = ["MakefileFormatter"] 6 | -------------------------------------------------------------------------------- /mbake/__main__.py: -------------------------------------------------------------------------------- 1 | """Entry point for running bake as a module with python -m bake.""" 2 | 3 | from .cli import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /tests/fixtures/colon_sensitive_assignment/input.mk: -------------------------------------------------------------------------------- 1 | URL = http://host/path 2 | TIME = 2025-10-13T12:34:56Z 3 | WIN = C:\\Windows\\System32 4 | VPATH = src:include:build 5 | -------------------------------------------------------------------------------- /tests/fixtures/colon_sensitive_assignment/expected.mk: -------------------------------------------------------------------------------- 1 | URL = http://host/path 2 | TIME = 2025-10-13T12:34:56Z 3 | WIN = C:\\Windows\\System32 4 | VPATH = src:include:build 5 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes/input.mk: -------------------------------------------------------------------------------- 1 | one: 2 | SOME_URL=http://github.com \ 3 | some_url_using_command 4 | 5 | two: 6 | SOME_URL=http://github.com \ 7 | some_url_using_command 8 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes/expected.mk: -------------------------------------------------------------------------------- 1 | one: 2 | SOME_URL=http://github.com \ 3 | some_url_using_command 4 | 5 | two: 6 | SOME_URL=http://github.com \ 7 | some_url_using_command 8 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes_quoted/expected.mk: -------------------------------------------------------------------------------- 1 | one: 2 | SOME_URL="http://github.com" \ 3 | some_url_using_command 4 | 5 | two: 6 | SOME_URL="http://github.com" \ 7 | some_url_using_command 8 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes_quoted/input.mk: -------------------------------------------------------------------------------- 1 | one: 2 | SOME_URL="http://github.com" \ 3 | some_url_using_command 4 | 5 | two: 6 | SOME_URL="http://github.com" \ 7 | some_url_using_command 8 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes_datetime/input.mk: -------------------------------------------------------------------------------- 1 | one: 2 | BUILD_TIME=2025-10-13T12:34:56Z \ 3 | echo "time: $$BUILD_TIME" 4 | 5 | two: 6 | BUILD_TIME=2025-10-13T12:34:56Z \ 7 | echo "time: $$BUILD_TIME" 8 | -------------------------------------------------------------------------------- /tests/fixtures/urls_in_recipes_datetime/expected.mk: -------------------------------------------------------------------------------- 1 | one: 2 | BUILD_TIME=2025-10-13T12:34:56Z \ 3 | echo "time: $$BUILD_TIME" 4 | 5 | two: 6 | BUILD_TIME=2025-10-13T12:34:56Z \ 7 | echo "time: $$BUILD_TIME" 8 | -------------------------------------------------------------------------------- /tests/fixtures/conditional_urls/expected.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | win: 3 | SOME_URL=http://github.com \ 4 | echo windows 5 | else 6 | unix: 7 | SOME_URL=http://github.com \ 8 | echo unix 9 | endif 10 | -------------------------------------------------------------------------------- /tests/fixtures/conditional_urls/input.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | win: 3 | SOME_URL=http://github.com \ 4 | echo windows 5 | else 6 | unix: 7 | SOME_URL=http://github.com \ 8 | echo unix 9 | endif 10 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_variations/expected.mk: -------------------------------------------------------------------------------- 1 | # Three equivalent VPATH styles - normalized to colon-separated when colons present 2 | VPATH = src include build 3 | VPATH = src:include:build 4 | VPATH = src:include:build 5 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_variations/input.mk: -------------------------------------------------------------------------------- 1 | # Three equivalent VPATH styles - normalized to colon-separated when colons present 2 | VPATH = src include build 3 | VPATH = src:include:build 4 | VPATH = src: include:build 5 | -------------------------------------------------------------------------------- /tests/fixtures/recipeprefix_multi/input.mk: -------------------------------------------------------------------------------- 1 | .RECIPEPREFIX := > 2 | first: 3 | >SOME_URL=http://example.com \ 4 | > echo first 5 | 6 | .RECIPEPREFIX := @ 7 | second: 8 | @BUILD_TIME=2025-10-13T12:34:56Z \ 9 | @ echo second 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/suffix_phony_issue/input.mk: -------------------------------------------------------------------------------- 1 | # Original issue: suffix rules should NOT be suggested as phony 2 | .POSIX: 3 | .SUFFIXES: .a .b 4 | 5 | all: foo.b 6 | foo.b: foo.a 7 | 8 | .a.b: 9 | cp $< $@ 10 | 11 | .PHONY: all 12 | -------------------------------------------------------------------------------- /tests/fixtures/recipeprefix_multi/expected.mk: -------------------------------------------------------------------------------- 1 | .RECIPEPREFIX := > 2 | first: 3 | >SOME_URL=http://example.com \ 4 | > echo first 5 | 6 | .RECIPEPREFIX := @ 7 | second: 8 | @BUILD_TIME=2025-10-13T12:34:56Z \ 9 | @ echo second 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/suffix_phony_issue/expected.mk: -------------------------------------------------------------------------------- 1 | # Original issue: suffix rules should NOT be suggested as phony 2 | .POSIX: 3 | .SUFFIXES: .a .b 4 | 5 | all: foo.b 6 | foo.b: foo.a 7 | 8 | .a.b: 9 | cp $< $@ 10 | 11 | .PHONY: all 12 | -------------------------------------------------------------------------------- /tests/fixtures/whitespace_normalization/expected.mk: -------------------------------------------------------------------------------- 1 | # Test whitespace normalization 2 | CC = gcc 3 | CFLAGS = -Wall -Wextra 4 | 5 | all: $(TARGET) 6 | echo "Building..." 7 | 8 | clean: 9 | rm -f *.o 10 | 11 | install: 12 | cp $(TARGET) /usr/local/bin/ 13 | -------------------------------------------------------------------------------- /tests/fixtures/define_endef/input.mk: -------------------------------------------------------------------------------- 1 | define YAML 2 | key: 1 3 | endef 4 | 5 | .ONESHELL: 6 | SHELL := bash 7 | 8 | .PHONY: test_split 9 | test_split: 10 | files=$$(shell ls); \ 11 | $(first) 12 | 13 | define first 14 | FIRST=$(word 1, $(subst _, ,$@)) 15 | echo "$${FIRST}" 16 | endef 17 | -------------------------------------------------------------------------------- /tests/fixtures/whitespace_normalization/input.mk: -------------------------------------------------------------------------------- 1 | # Test whitespace normalization 2 | CC = gcc 3 | CFLAGS = -Wall -Wextra 4 | 5 | all: $(TARGET) 6 | echo "Building..." 7 | 8 | 9 | clean: 10 | rm -f *.o 11 | 12 | 13 | 14 | install: 15 | cp $(TARGET) /usr/local/bin/ -------------------------------------------------------------------------------- /tests/fixtures/define_endef/expected.mk: -------------------------------------------------------------------------------- 1 | define YAML 2 | key: 1 3 | endef 4 | 5 | .ONESHELL: 6 | SHELL := bash 7 | 8 | .PHONY: test_split 9 | test_split: 10 | files=$$(shell ls); \ 11 | $(first) 12 | 13 | define first 14 | FIRST=$(word 1, $(subst _, ,$@)) 15 | echo "$${FIRST}" 16 | endef 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include INSTALLATION_GUIDE.md 4 | include .bake.toml.example 5 | recursive-include tests *.py *.mk 6 | recursive-include bake *.py 7 | global-exclude __pycache__ 8 | global-exclude *.py[co] 9 | global-exclude .pytest_cache 10 | global-exclude .coverage 11 | global-exclude htmlcov -------------------------------------------------------------------------------- /mbake/__init__.py: -------------------------------------------------------------------------------- 1 | """mbake - A Python-based Makefile formatter and linter.""" 2 | 3 | __version__ = "1.4.3" 4 | __author__ = "mbake Contributors" 5 | __description__ = "A Python-based Makefile formatter and linter" 6 | 7 | from .config import Config 8 | from .core.formatter import MakefileFormatter 9 | 10 | __all__ = ["MakefileFormatter", "Config"] 11 | -------------------------------------------------------------------------------- /tests/verilator/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check 2 | 3 | check: 4 | python3 -m mbake format --diff Makefile_obj-2.in 5 | python3 -m mbake format --diff Makefile-2.in 6 | python3 -m mbake format --diff Makefile-3.in 7 | python3 -m mbake format --diff Makefile-3.txt 8 | python3 -m mbake format --diff Makefile.txt 9 | python3 -m mbake format --diff verilated.mk-2.in 10 | -------------------------------------------------------------------------------- /tests/fixtures/phony_targets/input.mk: -------------------------------------------------------------------------------- 1 | # Test phony target formatting with scattered declarations 2 | all: $(TARGET) 3 | @echo "Build complete" 4 | 5 | .PHONY: clean 6 | clean: 7 | rm -f *.o $(TARGET) 8 | 9 | test: $(TARGET) 10 | ./run_tests.sh 11 | 12 | .PHONY: test install 13 | install: $(TARGET) 14 | cp $(TARGET) /usr/local/bin/ 15 | 16 | .PHONY: all 17 | help: 18 | @echo "Available targets: all, clean, test, install, help" -------------------------------------------------------------------------------- /tests/fixtures/phony_targets/expected.mk: -------------------------------------------------------------------------------- 1 | # Test phony target formatting with scattered declarations 2 | all: $(TARGET) 3 | @echo "Build complete" 4 | 5 | .PHONY: clean 6 | clean: 7 | rm -f *.o $(TARGET) 8 | 9 | test: $(TARGET) 10 | ./run_tests.sh 11 | 12 | .PHONY: test install 13 | install: $(TARGET) 14 | cp $(TARGET) /usr/local/bin/ 15 | 16 | .PHONY: all 17 | help: 18 | @echo "Available targets: all, clean, test, install, help" 19 | -------------------------------------------------------------------------------- /tests/fixtures/pattern_rules/input.mk: -------------------------------------------------------------------------------- 1 | # Test pattern rule formatting 2 | %.o:%.c 3 | $(CC) $(CFLAGS) -c -o $@ $< 4 | 5 | %.a: %.o 6 | $(AR) $(ARFLAGS) $@ $^ 7 | 8 | # Static pattern rule 9 | $(OBJECTS): %.o : %.c 10 | $(CC) $(CFLAGS) -c -o $@ $< 11 | 12 | # Multiple pattern rules 13 | %.d: %.c %.h 14 | $(CC) -MM $(CFLAGS) $< > $@ 15 | 16 | # Add a default target to resolve the "No targets" error 17 | .PHONY: all 18 | all: 19 | @echo "Makefile processed successfully." -------------------------------------------------------------------------------- /tests/fixtures/pattern_rules/expected.mk: -------------------------------------------------------------------------------- 1 | # Test pattern rule formatting 2 | %.o: %.c 3 | $(CC) $(CFLAGS) -c -o $@ $< 4 | 5 | %.a: %.o 6 | $(AR) $(ARFLAGS) $@ $^ 7 | 8 | # Static pattern rule 9 | $(OBJECTS): %.o: %.c 10 | $(CC) $(CFLAGS) -c -o $@ $< 11 | 12 | # Multiple pattern rules 13 | %.d: %.c %.h 14 | $(CC) -MM $(CFLAGS) $< > $@ 15 | 16 | # Add a default target to resolve the "No targets" error 17 | .PHONY: all 18 | all: 19 | @echo "Makefile processed successfully." 20 | -------------------------------------------------------------------------------- /tests/fixtures/pattern_rules/input_backup.mk: -------------------------------------------------------------------------------- 1 | # Test pattern rule formatting 2 | %.o:%.c 3 | $(CC) $(CFLAGS) -c -o $@ $< 4 | 5 | %.a: %.o 6 | $(AR) $(ARFLAGS) $@ $^ 7 | 8 | # Static pattern rule 9 | $(OBJECTS): %.o : %.c 10 | $(CC) $(CFLAGS) -c -o $@ $< 11 | 12 | # Multiple pattern rules 13 | %.d: %.c %.h 14 | $(CC) -MM $(CFLAGS) $< > $@ 15 | 16 | # Add a default target to resolve the "No targets" error 17 | .PHONY: all 18 | all: 19 | @echo "Makefile processed successfully." -------------------------------------------------------------------------------- /tests/fixtures/shell_formatting/input.mk: -------------------------------------------------------------------------------- 1 | # Test shell script formatting in recipes 2 | test: 3 | # Complex shell command with inconsistent indentation 4 | if [ "$(DEBUG)" = "yes" ]; then \ 5 | echo "Debug mode enabled"; \ 6 | CFLAGS="-g -O0"; \ 7 | else \ 8 | CFLAGS="-O2"; \ 9 | fi; \ 10 | $(CC) $$CFLAGS -o $(TARGET) $(SOURCES) 11 | 12 | deploy: 13 | for file in $(wildcard *.txt); do \ 14 | cat $$file | \ 15 | sed 's/foo/bar/g' > \ 16 | processed_$$(basename $$file); \ 17 | done -------------------------------------------------------------------------------- /tests/fixtures/shell_formatting/expected.mk: -------------------------------------------------------------------------------- 1 | # Test shell script formatting in recipes 2 | test: 3 | # Complex shell command with inconsistent indentation 4 | if [ "$(DEBUG)" = "yes" ]; then \ 5 | echo "Debug mode enabled"; \ 6 | CFLAGS="-g -O0"; \ 7 | else \ 8 | CFLAGS="-O2"; \ 9 | fi; \ 10 | $(CC) $$CFLAGS -o $(TARGET) $(SOURCES) 11 | 12 | deploy: 13 | for file in $(wildcard *.txt); do \ 14 | cat $$file | \ 15 | sed 's/foo/bar/g' > \ 16 | processed_$$(basename $$file); \ 17 | done 18 | -------------------------------------------------------------------------------- /tests/fixtures/makefile_vars_in_shell/input.mk: -------------------------------------------------------------------------------- 1 | # Test Makefile variables in shell commands 2 | TARGET = myapp 3 | SOURCES = $(wildcard *.c) 4 | 5 | build: 6 | @echo "Building $(TARGET)" 7 | for src in $(SOURCES); do \ 8 | echo "Compiling $$src"; \ 9 | $(CC) -c $$src -o $${src%.c}.o; \ 10 | done 11 | $(CC) -o $(TARGET) *.o 12 | 13 | test: 14 | ./$(TARGET) --test-mode 15 | if [ $$? -eq 0 ]; then \ 16 | echo "$(TARGET) tests passed"; \ 17 | else \ 18 | echo "$(TARGET) tests failed"; \ 19 | exit 1; \ 20 | fi -------------------------------------------------------------------------------- /tests/fixtures/makefile_vars_in_shell/expected.mk: -------------------------------------------------------------------------------- 1 | # Test Makefile variables in shell commands 2 | TARGET = myapp 3 | SOURCES = $(wildcard *.c) 4 | 5 | build: 6 | @echo "Building $(TARGET)" 7 | for src in $(SOURCES); do \ 8 | echo "Compiling $$src"; \ 9 | $(CC) -c $$src -o $${src%.c}.o; \ 10 | done 11 | $(CC) -o $(TARGET) *.o 12 | 13 | test: 14 | ./$(TARGET) --test-mode 15 | if [ $$? -eq 0 ]; then \ 16 | echo "$(TARGET) tests passed"; \ 17 | else \ 18 | echo "$(TARGET) tests failed"; \ 19 | exit 1; \ 20 | fi 21 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: mbake-format 2 | name: mbake format 3 | description: "Run 'mbake format' for Makefile formatting" 4 | entry: mbake format 5 | language: python 6 | types: [makefile] 7 | args: [] 8 | require_serial: true 9 | additional_dependencies: [] 10 | 11 | - id: mbake-validate 12 | name: mbake validate 13 | description: "Run 'mbake validate' for Makefile validation" 14 | entry: mbake validate 15 | language: python 16 | types: [makefile] 17 | args: [] 18 | require_serial: true 19 | additional_dependencies: [] 20 | -------------------------------------------------------------------------------- /mbake/constants/__init__.py: -------------------------------------------------------------------------------- 1 | """Constants package for mbake configuration and patterns.""" 2 | 3 | from .makefile_targets import ALL_SPECIAL_MAKE_TARGETS, DECLARATIVE_TARGETS 4 | from .phony_targets import COMMON_PHONY_TARGETS 5 | from .shell_commands import ( 6 | FILE_CREATING_COMMANDS, 7 | NON_FILE_CREATING_COMMANDS, 8 | SHELL_COMMAND_INDICATORS, 9 | ) 10 | 11 | __all__ = [ 12 | "DECLARATIVE_TARGETS", 13 | "COMMON_PHONY_TARGETS", 14 | "ALL_SPECIAL_MAKE_TARGETS", 15 | "SHELL_COMMAND_INDICATORS", 16 | "FILE_CREATING_COMMANDS", 17 | "NON_FILE_CREATING_COMMANDS", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_conditionals/expected.mk: -------------------------------------------------------------------------------- 1 | # VPATH in conditional blocks 2 | ifeq ($(PLATFORM),linux) 3 | VPATH = src:include:build 4 | else 5 | VPATH = src include build 6 | endif 7 | 8 | # VPATH with different assignment operators 9 | VPATH := src:include:build 10 | VPATH += src:include:build 11 | VPATH ?= src:include:build 12 | 13 | # VPATH with variable references 14 | SRC_DIRS = src include build 15 | VPATH = $(SRC_DIRS) 16 | 17 | # VPATH in nested conditionals 18 | ifdef DEBUG 19 | ifeq ($(OS),windows) 20 | VPATH = src:include:build 21 | else 22 | VPATH = src include build 23 | endif 24 | endif 25 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_conditionals/input.mk: -------------------------------------------------------------------------------- 1 | # VPATH in conditional blocks 2 | ifeq ($(PLATFORM),linux) 3 | VPATH = src:include:build 4 | else 5 | VPATH = src include build 6 | endif 7 | 8 | # VPATH with different assignment operators 9 | VPATH := src:include:build 10 | VPATH += src:include:build 11 | VPATH ?= src:include:build 12 | 13 | # VPATH with variable references 14 | SRC_DIRS = src include build 15 | VPATH = $(SRC_DIRS) 16 | 17 | # VPATH in nested conditionals 18 | ifdef DEBUG 19 | ifeq ($(OS),windows) 20 | VPATH = src: include:build 21 | else 22 | VPATH = src include build 23 | endif 24 | endif 25 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_edge_cases/expected.mk: -------------------------------------------------------------------------------- 1 | # VPATH edge cases and error scenarios 2 | VPATH = 3 | VPATH = src:include:build 4 | VPATH = src:include:build 5 | VPATH = src:include:build 6 | VPATH = src:include:build 7 | VPATH = src:include:build 8 | 9 | # VPATH with quotes (should be preserved) 10 | VPATH = "src:include:build" 11 | VPATH = 'src include build' 12 | 13 | # VPATH with escaped characters 14 | VPATH = src\:include\:build 15 | VPATH = src\ include\ build 16 | 17 | # VPATH with special characters in directory names 18 | VPATH = src-dir:include_dir:build.dir 19 | VPATH = src+dir:include-dir:build_dir 20 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_edge_cases/input.mk: -------------------------------------------------------------------------------- 1 | # VPATH edge cases and error scenarios 2 | VPATH = 3 | VPATH = src:include:build:: 4 | VPATH = :src:include:build: 5 | VPATH = src::include::build 6 | VPATH = src: include: build 7 | VPATH = src : include : build 8 | 9 | # VPATH with quotes (should be preserved) 10 | VPATH = "src:include:build" 11 | VPATH = 'src include build' 12 | 13 | # VPATH with escaped characters 14 | VPATH = src\:include\:build 15 | VPATH = src\ include\ build 16 | 17 | # VPATH with special characters in directory names 18 | VPATH = src-dir:include_dir:build.dir 19 | VPATH = src+dir:include-dir:build_dir 20 | -------------------------------------------------------------------------------- /tests/fixtures/suffix_rules/input.mk: -------------------------------------------------------------------------------- 1 | # Test suffix rules and SUFFIXES handling 2 | .POSIX: 3 | .SUFFIXES: .a .b .c .o 4 | 5 | # Phony targets 6 | all: foo.b bar.o 7 | clean: 8 | rm -f *.b *.o 9 | 10 | # File targets (should NOT be phony) 11 | foo.b: foo.a 12 | bar.o: bar.c 13 | 14 | # Suffix rules (should NOT be phony) 15 | .a.b: 16 | cp $< $@ 17 | 18 | .c.o: 19 | $(CC) -c $(CFLAGS) -o $@ $< 20 | 21 | # Pattern rules (should NOT be phony) 22 | %.h: %.c 23 | @echo "Generating $@ from $<" 24 | 25 | # Static pattern rules (should NOT be phony) 26 | objects: %.o: %.c 27 | $(CC) -c $(CFLAGS) -o $@ $< 28 | 29 | .PHONY: all clean 30 | -------------------------------------------------------------------------------- /tests/fixtures/suffix_rules/expected.mk: -------------------------------------------------------------------------------- 1 | # Test suffix rules and SUFFIXES handling 2 | .POSIX: 3 | .SUFFIXES: .a .b .c .o 4 | 5 | # Phony targets 6 | all: foo.b bar.o 7 | clean: 8 | rm -f *.b *.o 9 | 10 | # File targets (should NOT be phony) 11 | foo.b: foo.a 12 | bar.o: bar.c 13 | 14 | # Suffix rules (should NOT be phony) 15 | .a.b: 16 | cp $< $@ 17 | 18 | .c.o: 19 | $(CC) -c $(CFLAGS) -o $@ $< 20 | 21 | # Pattern rules (should NOT be phony) 22 | %.h: %.c 23 | @echo "Generating $@ from $<" 24 | 25 | # Static pattern rules (should NOT be phony) 26 | objects: %.o: %.c 27 | $(CC) -c $(CFLAGS) -o $@ $< 28 | 29 | .PHONY: all clean 30 | -------------------------------------------------------------------------------- /tests/fixtures/target_spacing/expected.mk: -------------------------------------------------------------------------------- 1 | # Test target definition spacing 2 | .PHONY: all target1 target2 empty-target standalone dep1 dep2 dep3 dep4 3 | 4 | all: target1 target2 5 | @echo "All targets" 6 | 7 | target1: dep1 dep2 8 | echo "Target 1" 9 | 10 | target2: dep3 dep4 11 | echo "Target 2" 12 | 13 | # Empty target 14 | empty-target: 15 | 16 | # Target with no dependencies 17 | standalone: 18 | echo "Standalone" 19 | 20 | # Phony dependencies (added to resolve the error) 21 | dep1: 22 | echo "Processing dep1" 23 | dep2: 24 | echo "Processing dep2" 25 | dep3: 26 | echo "Processing dep3" 27 | dep4: 28 | echo "Processing dep4" 29 | -------------------------------------------------------------------------------- /tests/fixtures/target_spacing/input.mk: -------------------------------------------------------------------------------- 1 | # Test target definition spacing 2 | .PHONY: all target1 target2 empty-target standalone dep1 dep2 dep3 dep4 3 | 4 | all:target1 target2 5 | @echo "All targets" 6 | 7 | target1 : dep1 dep2 8 | echo "Target 1" 9 | 10 | target2:dep3 dep4 11 | echo "Target 2" 12 | 13 | # Empty target 14 | empty-target : 15 | 16 | 17 | # Target with no dependencies 18 | standalone: 19 | echo "Standalone" 20 | 21 | # Phony dependencies (added to resolve the error) 22 | dep1: 23 | echo "Processing dep1" 24 | dep2: 25 | echo "Processing dep2" 26 | dep3: 27 | echo "Processing dep3" 28 | dep4: 29 | echo "Processing dep4" -------------------------------------------------------------------------------- /tests/fixtures/target_spacing/input_backup.mk: -------------------------------------------------------------------------------- 1 | # Test target definition spacing 2 | .PHONY: all target1 target2 empty-target standalone dep1 dep2 dep3 dep4 3 | 4 | all:target1 target2 5 | @echo "All targets" 6 | 7 | target1 : dep1 dep2 8 | echo "Target 1" 9 | 10 | target2:dep3 dep4 11 | echo "Target 2" 12 | 13 | # Empty target 14 | empty-target : 15 | 16 | 17 | # Target with no dependencies 18 | standalone: 19 | echo "Standalone" 20 | 21 | # Phony dependencies (added to resolve the error) 22 | dep1: 23 | echo "Processing dep1" 24 | dep2: 25 | echo "Processing dep2" 26 | dep3: 27 | echo "Processing dep3" 28 | dep4: 29 | echo "Processing dep4" -------------------------------------------------------------------------------- /tests/fixtures/vpath_advanced/input.mk: -------------------------------------------------------------------------------- 1 | # VPATH with function calls 2 | VPATH = $(wildcard src/*) $(wildcard include/*) 3 | VPATH = $(patsubst %/,%,$(dir $(wildcard src/*.c))) 4 | 5 | # VPATH in target-specific context 6 | debug: VPATH = src:include:build 7 | release: VPATH = src include build 8 | test: VPATH = src: include:build 9 | 10 | # VPATH with complex variable references 11 | BASE_DIRS = src include 12 | EXTRA_DIRS = build test 13 | VPATH = $(BASE_DIRS):$(EXTRA_DIRS) 14 | 15 | # VPATH with substitution references 16 | DIRS = src:include:build 17 | VPATH = $(DIRS) 18 | 19 | # VPATH with conditional assignment and function calls 20 | VPATH ?= $(wildcard src/*) $(wildcard include/*) 21 | -------------------------------------------------------------------------------- /tests/fixtures/vpath_advanced/expected.mk: -------------------------------------------------------------------------------- 1 | # VPATH with function calls 2 | VPATH = $(wildcard src/*) $(wildcard include/*) 3 | VPATH = $(patsubst %/,%,$(dir $(wildcard src/*.c))) 4 | 5 | # VPATH in target-specific context 6 | debug: VPATH = src:include:build 7 | release: VPATH = src include build 8 | test: VPATH = src:include:build 9 | 10 | # VPATH with complex variable references 11 | BASE_DIRS = src include 12 | EXTRA_DIRS = build test 13 | VPATH = $(BASE_DIRS):$(EXTRA_DIRS) 14 | 15 | # VPATH with substitution references 16 | DIRS = src:include:build 17 | VPATH = $(DIRS) 18 | 19 | # VPATH with conditional assignment and function calls 20 | VPATH ?= $(wildcard src/*) $(wildcard include/*) 21 | -------------------------------------------------------------------------------- /tests/fixtures/line_continuations/expected.mk: -------------------------------------------------------------------------------- 1 | # Test line continuation formatting 2 | SOURCES = main.c utils.c parser.c 3 | 4 | # Line continuation in recipe with trailing spaces 5 | build: 6 | echo "Starting build" && \ 7 | mkdir -p $(BUILD_DIR) && \ 8 | $(CC) $(CFLAGS) \ 9 | -o $(TARGET) \ 10 | $(SOURCES) 11 | 12 | # Minimal test for tab/space/continuation issues 13 | 14 | ifeq ($(CFG),yes) 15 | foo: 16 | @echo "Hello world" \ 17 | continued line \ 18 | another line 19 | else 20 | foo: 21 | @echo "Alt" \ 22 | alt continued 23 | endif 24 | 25 | # Comments at left margin should not affect indentation 26 | # This is a left comment 27 | foo: 28 | @echo "Should still be tabbed" 29 | -------------------------------------------------------------------------------- /tests/fixtures/line_continuations/input.mk: -------------------------------------------------------------------------------- 1 | # Test line continuation formatting 2 | SOURCES = main.c utils.c parser.c 3 | 4 | # Line continuation in recipe with trailing spaces 5 | build: 6 | echo "Starting build" && \ 7 | mkdir -p $(BUILD_DIR) && \ 8 | $(CC) $(CFLAGS) \ 9 | -o $(TARGET) \ 10 | $(SOURCES) 11 | 12 | # Minimal test for tab/space/continuation issues 13 | 14 | ifeq ($(CFG),yes) 15 | foo: 16 | @echo "Hello world" \ 17 | continued line \ 18 | another line 19 | else 20 | foo: 21 | @echo "Alt" \ 22 | alt continued 23 | endif 24 | 25 | # Comments at left margin should not affect indentation 26 | # This is a left comment 27 | foo: 28 | @echo "Should still be tabbed" 29 | -------------------------------------------------------------------------------- /tests/fixtures/nested_conditional_alignment/input.mk: -------------------------------------------------------------------------------- 1 | all: 2 | ifdef VAR1 3 | ifneq ($(VAR2),value2) 4 | ifdef VAR3 5 | @echo " VAR3 is defined" 6 | ifndef VAR4 7 | @echo " VAR4 is NOT defined" 8 | ifeq ($(VAR5),value5) 9 | @echo " VAR5 matches value5" 10 | ifneq ($(VAR6),value6) 11 | ifeq ($(VAR7),value7) 12 | @echo "Deeply nested VAR7 check" 13 | else 14 | @echo "VAR7 else branch" 15 | endif 16 | else 17 | @echo "VAR6 else branch" 18 | endif 19 | else 20 | @echo "VAR5 else branch" 21 | endif 22 | endif 23 | else 24 | @echo "VAR3 else branch" 25 | endif 26 | else 27 | @echo "VAR2 else branch" 28 | endif 29 | else 30 | @echo "VAR1 else branch" 31 | endif -------------------------------------------------------------------------------- /mbake/constants/phony_targets.py: -------------------------------------------------------------------------------- 1 | """Common phony target names for automatic detection.""" 2 | 3 | # Common phony target names that should be automatically detected 4 | # These are action-oriented targets that don't represent actual files 5 | # Only includes targets that are almost certainly phony and won't conflict with file targets 6 | COMMON_PHONY_TARGETS = { 7 | "all", 8 | "clean", 9 | "install", 10 | "uninstall", 11 | "test", 12 | "help", 13 | "build", 14 | "rebuild", 15 | "debug", 16 | "release", 17 | "dist", 18 | "distclean", 19 | "docs", 20 | "doc", 21 | "lint", 22 | "setup", 23 | "format", 24 | "check", 25 | "verify", 26 | "validate", 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/recipe_tabs/expected.mk: -------------------------------------------------------------------------------- 1 | # Test that recipes use tabs, not spaces 2 | all: build test 3 | 4 | build: 5 | echo "This line should use a tab, not spaces" 6 | gcc -o hello hello.c \ 7 | -Wall \ 8 | -Werror 9 | 10 | test: build 11 | echo "This line already has a tab" 12 | echo "This line has spaces but should be converted to tab" \ 13 | --long-arg \ 14 | --another-arg 15 | 16 | clean: 17 | rm -f hello 18 | # This comment has spaces instead of a tab 19 | 20 | # Test shell command indentation preservation 21 | complex-cmd: 22 | perl -p -i -e 'use File::Spec;' \ 23 | -e' $$path = File::Spec->abs2rel("$(path)");' \ 24 | -e's/my \$$var = .*/my \$$var = "$$path";/g' \ 25 | -- "$(file)" 26 | -------------------------------------------------------------------------------- /mbake/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utility modules for mbake formatting operations.""" 2 | 3 | from .format_disable import FormatDisableHandler, FormatRegion 4 | from .line_utils import LineUtils, MakefileParser, PhonyAnalyzer 5 | from .pattern_utils import PatternUtils 6 | from .version_utils import ( 7 | VersionError, 8 | check_for_updates, 9 | get_pypi_version, 10 | is_development_install, 11 | update_package, 12 | ) 13 | 14 | __all__ = [ 15 | "FormatDisableHandler", 16 | "FormatRegion", 17 | "LineUtils", 18 | "MakefileParser", 19 | "PhonyAnalyzer", 20 | "PatternUtils", 21 | "check_for_updates", 22 | "get_pypi_version", 23 | "update_package", 24 | "is_development_install", 25 | "VersionError", 26 | ] 27 | -------------------------------------------------------------------------------- /tests/fixtures/recipe_tabs/input.mk: -------------------------------------------------------------------------------- 1 | # Test that recipes use tabs, not spaces 2 | all: build test 3 | 4 | build: 5 | echo "This line should use a tab, not spaces" 6 | gcc -o hello hello.c \ 7 | -Wall \ 8 | -Werror 9 | 10 | test: build 11 | echo "This line already has a tab" 12 | echo "This line has spaces but should be converted to tab" \ 13 | --long-arg \ 14 | --another-arg 15 | 16 | clean: 17 | rm -f hello 18 | # This comment has spaces instead of a tab 19 | 20 | # Test shell command indentation preservation 21 | complex-cmd: 22 | perl -p -i -e 'use File::Spec;' \ 23 | -e' $$path = File::Spec->abs2rel("$(path)");' \ 24 | -e's/my \$$var = .*/my \$$var = "$$path";/g' \ 25 | -- "$(file)" -------------------------------------------------------------------------------- /tests/fixtures/complex/expected.mk: -------------------------------------------------------------------------------- 1 | # Complex Makefile with multiple formatting issues 2 | CC = gcc 3 | CFLAGS := -Wall -Wextra 4 | LDFLAGS = -lpthread 5 | 6 | SOURCES = main.c \ 7 | utils.c \ 8 | parser.c 9 | 10 | .PHONY: clean 11 | all: $(TARGET) 12 | echo "Building project" 13 | $(CC) $(CFLAGS) -o $(TARGET) $(SOURCES) 14 | 15 | .PHONY: test 16 | # Conditional with poor formatting 17 | ifeq ($(DEBUG),yes) 18 | EXTRA_FLAGS = -g -O0 19 | else 20 | EXTRA_FLAGS=-O2 21 | endif 22 | 23 | test: $(TARGET) 24 | if [ -f $(TARGET) ]; then \ 25 | echo "Running tests"; \ 26 | ./$(TARGET) --test; \ 27 | else \ 28 | echo "Binary not found"; \ 29 | exit 1; \ 30 | fi 31 | 32 | clean: 33 | rm -f *.o $(TARGET) 34 | 35 | .PHONY: all install 36 | install: $(TARGET) 37 | cp $(TARGET) /usr/local/bin/ 38 | -------------------------------------------------------------------------------- /tests/fixtures/variable_assignments/input.mk: -------------------------------------------------------------------------------- 1 | # Test variable assignment formatting 2 | CC=gcc 3 | CXX := g++ 4 | LD=$(CC) 5 | CFLAGS = -Wall -Wextra -O2 6 | CXXFLAGS=$(CFLAGS) -std=c++17 7 | LDFLAGS=-lpthread 8 | 9 | # Multi-line variable assignment 10 | SOURCES = main.c \ 11 | utils.c \ 12 | parser.c 13 | 14 | # Function with nested calls 15 | OBJECTS=$(patsubst %.c,%.o,$(filter %.c,$(SOURCES))) 16 | 17 | # Add a default target 18 | .PHONY: all 19 | all: 20 | @echo "Makefile processed successfully." 21 | 22 | # URL assignments should remain unchanged aside from spacing normalization 23 | VARIABLE = http://www.github.com 24 | VARIABLE = http://www.github.com 25 | 26 | # Variants with uneven spacing should normalize consistently 27 | VARIABLE= http://www.github.com 28 | VARIABLE =http://www.github.com -------------------------------------------------------------------------------- /tests/fixtures/invalid_targets/input.mk: -------------------------------------------------------------------------------- 1 | # Invalid target syntax test cases 2 | 3 | # Invalid target with = sign 4 | target=value: prerequisites 5 | recipe 6 | 7 | # Invalid target with .RECIPEPREFIX character 8 | .RECIPEPREFIX := > 9 | >invalid: prerequisites 10 | recipe 11 | 12 | # Another invalid target with custom prefix 13 | .RECIPEPREFIX := @ 14 | @another_invalid: deps 15 | recipe 16 | 17 | # Valid targets for comparison 18 | valid_target: prerequisites 19 | recipe 20 | 21 | .RECIPEPREFIX := > 22 | valid_recipe: 23 | > echo "This is valid" 24 | 25 | # Invalid target in conditional 26 | ifeq ($(DEBUG),yes) 27 | debug_target=value: debug_deps 28 | debug_recipe 29 | endif 30 | 31 | # Valid target in conditional 32 | ifeq ($(RELEASE),yes) 33 | release_target: release_deps 34 | release_recipe 35 | endif 36 | -------------------------------------------------------------------------------- /tests/fixtures/invalid_targets/expected.mk: -------------------------------------------------------------------------------- 1 | # Invalid target syntax test cases 2 | 3 | # Invalid target with = sign 4 | target=value: prerequisites 5 | recipe 6 | 7 | # Invalid target with .RECIPEPREFIX character 8 | .RECIPEPREFIX := > 9 | >invalid: prerequisites 10 | recipe 11 | 12 | # Another invalid target with custom prefix 13 | .RECIPEPREFIX := @ 14 | @another_invalid: deps 15 | recipe 16 | 17 | # Valid targets for comparison 18 | valid_target: prerequisites 19 | recipe 20 | 21 | .RECIPEPREFIX := > 22 | valid_recipe: 23 | > echo "This is valid" 24 | 25 | # Invalid target in conditional 26 | ifeq ($(DEBUG),yes) 27 | debug_target=value: debug_deps 28 | debug_recipe 29 | endif 30 | 31 | # Valid target in conditional 32 | ifeq ($(RELEASE),yes) 33 | release_target: release_deps 34 | release_recipe 35 | endif 36 | -------------------------------------------------------------------------------- /tests/fixtures/variable_assignments/expected.mk: -------------------------------------------------------------------------------- 1 | # Test variable assignment formatting 2 | CC = gcc 3 | CXX := g++ 4 | LD = $(CC) 5 | CFLAGS = -Wall -Wextra -O2 6 | CXXFLAGS = $(CFLAGS) -std=c++17 7 | LDFLAGS = -lpthread 8 | 9 | # Multi-line variable assignment 10 | SOURCES = main.c \ 11 | utils.c \ 12 | parser.c 13 | 14 | # Function with nested calls 15 | OBJECTS = $(patsubst %.c,%.o,$(filter %.c,$(SOURCES))) 16 | 17 | # Add a default target 18 | .PHONY: all 19 | all: 20 | @echo "Makefile processed successfully." 21 | 22 | # URL assignments should remain unchanged aside from spacing normalization 23 | VARIABLE = http://www.github.com 24 | VARIABLE = http://www.github.com 25 | 26 | # Variants with uneven spacing should normalize consistently 27 | VARIABLE = http://www.github.com 28 | VARIABLE = http://www.github.com 29 | -------------------------------------------------------------------------------- /tests/fixtures/complex/input.mk: -------------------------------------------------------------------------------- 1 | # Complex Makefile with multiple formatting issues 2 | CC=gcc 3 | CFLAGS := -Wall -Wextra 4 | LDFLAGS= -lpthread 5 | 6 | SOURCES = main.c \ 7 | utils.c \ 8 | parser.c 9 | 10 | .PHONY: clean 11 | all: $(TARGET) 12 | echo "Building project" 13 | $(CC) $(CFLAGS) -o $(TARGET) $(SOURCES) 14 | 15 | .PHONY: test 16 | # Conditional with poor formatting 17 | ifeq ($(DEBUG),yes) 18 | EXTRA_FLAGS=-g -O0 19 | else 20 | EXTRA_FLAGS=-O2 21 | endif 22 | 23 | test: $(TARGET) 24 | if [ -f $(TARGET) ]; then \ 25 | echo "Running tests"; \ 26 | ./$(TARGET) --test; \ 27 | else \ 28 | echo "Binary not found"; \ 29 | exit 1; \ 30 | fi 31 | 32 | clean: 33 | rm -f *.o $(TARGET) 34 | 35 | .PHONY: all install 36 | install : $(TARGET) 37 | cp $(TARGET) /usr/local/bin/ -------------------------------------------------------------------------------- /tests/fixtures/nested_conditional_alignment/expected.mk: -------------------------------------------------------------------------------- 1 | all: 2 | ifdef VAR1 3 | ifneq ($(VAR2),value2) 4 | ifdef VAR3 5 | @echo " VAR3 is defined" 6 | ifndef VAR4 7 | @echo " VAR4 is NOT defined" 8 | ifeq ($(VAR5),value5) 9 | @echo " VAR5 matches value5" 10 | ifneq ($(VAR6),value6) 11 | ifeq ($(VAR7),value7) 12 | @echo "Deeply nested VAR7 check" 13 | else 14 | @echo "VAR7 else branch" 15 | endif 16 | else 17 | @echo "VAR6 else branch" 18 | endif 19 | else 20 | @echo "VAR5 else branch" 21 | endif 22 | endif 23 | else 24 | @echo "VAR3 else branch" 25 | endif 26 | else 27 | @echo "VAR2 else branch" 28 | endif 29 | else 30 | @echo "VAR1 else branch" 31 | endif 32 | -------------------------------------------------------------------------------- /tests/fixtures/nested_conditional_indentation/expected.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(CFG_WITH_LONGTESTS),yes) 2 | ifeq ($(DRIVER_STD),newest) 3 | CPPFLAGS += $(CFG_CXXFLAGS_STD) 4 | else 5 | CPPFLAGS += else_term 6 | endif 7 | ifneq ($(DRIVER_STD),newest) 8 | ifneq ($(DRIVER_STD),newest) 9 | CPPFLAGS += ifneq_term 10 | endif 11 | endif 12 | endif 13 | 14 | define TEST_SNAP_template 15 | mkdir -p $(TEST_SNAP_DIR) 16 | rm -rf $(TEST_SNAP_DIR)/obj_$(1) 17 | cp -r obj_$(1) $(TEST_SNAP_DIR)/ 18 | find $(TEST_SNAP_DIR)/obj_$(1) \( $(TEST_SNAP_IGNORE:%=-name "%" -o) \ 19 | -type f -executable \) -prune | xargs rm -r 20 | endef 21 | 22 | ifeq ($(FOO),yes) 23 | define FOO_template 24 | something 25 | endef 26 | else 27 | define FOO_template 28 | something_else 29 | endef 30 | endif 31 | 32 | .PHONY: all 33 | all: 34 | @echo "Makefile processed successfully." 35 | -------------------------------------------------------------------------------- /tests/fixtures/nested_conditional_indentation/input.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(CFG_WITH_LONGTESTS),yes) 2 | ifeq ($(DRIVER_STD),newest) 3 | CPPFLAGS += $(CFG_CXXFLAGS_STD) 4 | else 5 | CPPFLAGS += else_term 6 | endif 7 | ifneq ($(DRIVER_STD),newest) 8 | ifneq ($(DRIVER_STD),newest) 9 | CPPFLAGS += ifneq_term 10 | endif 11 | endif 12 | endif 13 | 14 | define TEST_SNAP_template 15 | mkdir -p $(TEST_SNAP_DIR) 16 | rm -rf $(TEST_SNAP_DIR)/obj_$(1) 17 | cp -r obj_$(1) $(TEST_SNAP_DIR)/ 18 | find $(TEST_SNAP_DIR)/obj_$(1) \( $(TEST_SNAP_IGNORE:%=-name "%" -o) \ 19 | -type f -executable \) -prune | xargs rm -r 20 | endef 21 | 22 | ifeq ($(FOO),yes) 23 | define FOO_template 24 | something 25 | endef 26 | else 27 | define FOO_template 28 | something_else 29 | endef 30 | endif 31 | 32 | .PHONY: all 33 | all: 34 | @echo "Makefile processed successfully." 35 | -------------------------------------------------------------------------------- /tests/fixtures/expected.mk: -------------------------------------------------------------------------------- 1 | # Sample Makefile with formatting issues 2 | CC := gcc 3 | CFLAGS = -Wall -Wextra -g 4 | SOURCES = main.c utils.c helper.c 5 | 6 | OBJECTS = $(SOURCES:.c=.o) 7 | TARGET = myprogram 8 | 9 | # All targets are now phony so they won't look for or create files 10 | .PHONY: all clean dist install test $(TARGET) $(OBJECTS) 11 | 12 | all: $(TARGET) 13 | 14 | $(TARGET): $(OBJECTS) 15 | @echo "Linking object files to create the executable: $(TARGET)" 16 | 17 | %.o: %.c 18 | @echo "Compiling C source file to object file: $<" 19 | 20 | clean: 21 | @echo "Removing object files and the executable" 22 | 23 | install: $(TARGET) 24 | @echo "Installing $(TARGET) to /usr/local/bin/" 25 | 26 | test: all 27 | @echo "Running tests for $(TARGET)" 28 | 29 | # Another phony target 30 | dist: 31 | @echo "Creating distribution archive: $(TARGET).tar.gz" 32 | -------------------------------------------------------------------------------- /tests/fixtures/input.mk: -------------------------------------------------------------------------------- 1 | # Sample Makefile with formatting issues 2 | CC:=gcc 3 | CFLAGS= -Wall -Wextra -g 4 | SOURCES=main.c utils.c helper.c 5 | 6 | OBJECTS=$(SOURCES:.c=.o) 7 | TARGET=myprogram 8 | 9 | # All targets are now phony so they won't look for or create files 10 | .PHONY: all clean dist install test $(TARGET) $(OBJECTS) 11 | 12 | all: $(TARGET) 13 | 14 | $(TARGET): $(OBJECTS) 15 | @echo "Linking object files to create the executable: $(TARGET)" 16 | 17 | %.o: %.c 18 | @echo "Compiling C source file to object file: $<" 19 | 20 | clean: 21 | @echo "Removing object files and the executable" 22 | 23 | install:$(TARGET) 24 | @echo "Installing $(TARGET) to /usr/local/bin/" 25 | 26 | test : all 27 | @echo "Running tests for $(TARGET)" 28 | 29 | # Another phony target 30 | dist: 31 | @echo "Creating distribution archive: $(TARGET).tar.gz" -------------------------------------------------------------------------------- /tests/fixtures/duplicate_targets_conditional/input.mk: -------------------------------------------------------------------------------- 1 | # Test duplicate target detection in conditional blocks 2 | ifneq ($(SYSTEMC_EXISTS),) 3 | default: run 4 | else 5 | default: nosc 6 | endif 7 | 8 | # Nested conditionals with same target names 9 | ifeq ($(OS),Windows) 10 | ifeq ($(ARCH),x64) 11 | build: build-windows-x64 12 | else 13 | build: build-windows-x86 14 | endif 15 | else 16 | ifeq ($(OS),Linux) 17 | build: build-linux 18 | endif 19 | endif 20 | 21 | # Targets after conditional blocks 22 | ifneq ($(FEATURE_X),) 23 | test: test-with-x 24 | else 25 | test: test-without-x 26 | endif 27 | 28 | clean: 29 | rm -f *.o 30 | 31 | # Real duplicate targets (should be flagged) 32 | install: 33 | echo "First install" 34 | echo "Second install" 35 | 36 | # Phony targets to resolve dependencies 37 | .PHONY: nosc run 38 | nosc: 39 | echo "No SystemC" 40 | run: 41 | echo "Running" 42 | -------------------------------------------------------------------------------- /tests/fixtures/duplicate_targets_conditional/expected.mk: -------------------------------------------------------------------------------- 1 | # Test duplicate target detection in conditional blocks 2 | ifneq ($(SYSTEMC_EXISTS),) 3 | default: run 4 | else 5 | default: nosc 6 | endif 7 | 8 | # Nested conditionals with same target names 9 | ifeq ($(OS),Windows) 10 | ifeq ($(ARCH),x64) 11 | build: build-windows-x64 12 | else 13 | build: build-windows-x86 14 | endif 15 | else 16 | ifeq ($(OS),Linux) 17 | build: build-linux 18 | endif 19 | endif 20 | 21 | # Targets after conditional blocks 22 | ifneq ($(FEATURE_X),) 23 | test: test-with-x 24 | else 25 | test: test-without-x 26 | endif 27 | 28 | clean: 29 | rm -f *.o 30 | 31 | # Real duplicate targets (should be flagged) 32 | install: 33 | echo "First install" 34 | echo "Second install" 35 | 36 | # Phony targets to resolve dependencies 37 | .PHONY: nosc run 38 | nosc: 39 | echo "No SystemC" 40 | run: 41 | echo "Running" 42 | -------------------------------------------------------------------------------- /tests/fixtures/format_disable/input.mk: -------------------------------------------------------------------------------- 1 | # bake-format off 2 | NO_FORMAT_1= \ 3 | 1 \ 4 | 45678 \ 5 | 6 | #bake-format on 7 | 8 | # bake-format off : optional comment 9 | NO_FORMAT_2= \ 10 | 1 \ 11 | 45678 \ 12 | 13 | #bake-format on 14 | 15 | # This will be formatted normally 16 | VAR1:=value1 17 | target1: 18 | echo 'spaces will become tabs' 19 | 20 | # bake-format off 21 | NO_FORMAT_1= \ 22 | 1 \ 23 | 45678 \ 24 | 25 | badly_spaced_target: 26 | echo 'these spaces will NOT become tabs' 27 | echo 'even weird indentation is preserved' 28 | .PHONY:not_grouped 29 | #bake-format on 30 | 31 | # This should be formatted again 32 | VAR2:=value2 33 | 34 | # bake-format off : optional comment 35 | ANOTHER_UNFORMATTED_VAR:=no_spaces_added_here 36 | weird_target_with_spaces: 37 | echo 'preserved as-is' 38 | # bake-format on 39 | 40 | # Back to normal formatting 41 | VAR3:=value3 42 | target3: 43 | echo 'back to normal' -------------------------------------------------------------------------------- /tests/fixtures/special_targets/input.mk: -------------------------------------------------------------------------------- 1 | # Test special target handling 2 | .POSIX: 3 | .SUFFIXES: .c .o .h 4 | 5 | # Global directives (should not be duplicated) 6 | # Note: .EXPORT_ALL_VARIABLES conflicts with .POSIX, so it's omitted 7 | .NOTPARALLEL: 8 | .ONESHELL: 9 | 10 | # Declarative targets (can be duplicated) 11 | .PHONY: all clean 12 | .PHONY: test install 13 | 14 | .SUFFIXES: .cpp .obj 15 | .SUFFIXES: .py .pyc 16 | 17 | # Rule behavior targets (can be duplicated) 18 | .PRECIOUS: *.o 19 | .INTERMEDIATE: temp.* 20 | .SECONDARY: backup.* 21 | .IGNORE: clean 22 | .SILENT: install 23 | 24 | # Utility targets 25 | .VARIABLES: 26 | .MAKE: 27 | .WAIT: 28 | .INCLUDE_DIRS: 29 | .LIBPATTERNS: lib%.a 30 | 31 | # Regular targets 32 | all: main.o 33 | $(CC) -o main main.o 34 | 35 | main.o: main.c 36 | $(CC) -c main.c 37 | 38 | clean: 39 | rm -f *.o main 40 | 41 | test: 42 | @echo "Running tests" 43 | 44 | install: main 45 | cp main /usr/local/bin/ 46 | -------------------------------------------------------------------------------- /tests/fixtures/format_disable/expected.mk: -------------------------------------------------------------------------------- 1 | # bake-format off 2 | NO_FORMAT_1= \ 3 | 1 \ 4 | 45678 \ 5 | 6 | #bake-format on 7 | 8 | # bake-format off : optional comment 9 | NO_FORMAT_2= \ 10 | 1 \ 11 | 45678 \ 12 | 13 | #bake-format on 14 | 15 | # This will be formatted normally 16 | VAR1 := value1 17 | target1: 18 | echo 'spaces will become tabs' 19 | 20 | # bake-format off 21 | NO_FORMAT_1= \ 22 | 1 \ 23 | 45678 \ 24 | 25 | badly_spaced_target: 26 | echo 'these spaces will NOT become tabs' 27 | echo 'even weird indentation is preserved' 28 | .PHONY:not_grouped 29 | #bake-format on 30 | 31 | # This should be formatted again 32 | VAR2 := value2 33 | 34 | # bake-format off : optional comment 35 | ANOTHER_UNFORMATTED_VAR:=no_spaces_added_here 36 | weird_target_with_spaces: 37 | echo 'preserved as-is' 38 | # bake-format on 39 | 40 | # Back to normal formatting 41 | VAR3 := value3 42 | target3: 43 | echo 'back to normal' 44 | -------------------------------------------------------------------------------- /tests/fixtures/special_targets/expected.mk: -------------------------------------------------------------------------------- 1 | # Test special target handling 2 | .POSIX: 3 | .SUFFIXES: .c .o .h 4 | 5 | # Global directives (should not be duplicated) 6 | # Note: .EXPORT_ALL_VARIABLES conflicts with .POSIX, so it's omitted 7 | .NOTPARALLEL: 8 | .ONESHELL: 9 | 10 | # Declarative targets (can be duplicated) 11 | .PHONY: all clean 12 | .PHONY: test install 13 | 14 | .SUFFIXES: .cpp .obj 15 | .SUFFIXES: .py .pyc 16 | 17 | # Rule behavior targets (can be duplicated) 18 | .PRECIOUS: *.o 19 | .INTERMEDIATE: temp.* 20 | .SECONDARY: backup.* 21 | .IGNORE: clean 22 | .SILENT: install 23 | 24 | # Utility targets 25 | .VARIABLES: 26 | .MAKE: 27 | .WAIT: 28 | .INCLUDE_DIRS: 29 | .LIBPATTERNS: lib%.a 30 | 31 | # Regular targets 32 | all: main.o 33 | $(CC) -o main main.o 34 | 35 | main.o: main.c 36 | $(CC) -c main.c 37 | 38 | clean: 39 | rm -f *.o main 40 | 41 | test: 42 | @echo "Running tests" 43 | 44 | install: main 45 | cp main /usr/local/bin/ 46 | -------------------------------------------------------------------------------- /tests/fixtures/backslash_continuation_block/expected.mk: -------------------------------------------------------------------------------- 1 | 2 | PY_PROGRAMS = \ 3 | bin/verilator_ccache_report \ 4 | bin/verilator_difftree \ 5 | bin/verilator_gantt \ 6 | bin/verilator_includer \ 7 | bin/verilator_profcfunc \ 8 | examples/json_py/vl_file_copy \ 9 | examples/json_py/vl_hier_graph \ 10 | docs/guide/conf.py \ 11 | docs/bin/vl_sphinx_extract \ 12 | docs/bin/vl_sphinx_fix \ 13 | src/astgen \ 14 | src/bisonpre \ 15 | src/config_rev \ 16 | src/cppcheck_filtered \ 17 | src/flexfix \ 18 | src/vlcovgen \ 19 | src/.gdbinit.py \ 20 | test_regress/*.py \ 21 | test_regress/t/*.pf \ 22 | nodist/clang_check_attributes \ 23 | nodist/code_coverage \ 24 | nodist/dot_importer \ 25 | nodist/fuzzer/actual_fail \ 26 | nodist/fuzzer/generate_dictionary \ 27 | nodist/install_test \ 28 | nodist/log_changes \ 29 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/backslash_continuation_block/input.mk: -------------------------------------------------------------------------------- 1 | 2 | PY_PROGRAMS = \ 3 | bin/verilator_ccache_report \ 4 | bin/verilator_difftree \ 5 | bin/verilator_gantt \ 6 | bin/verilator_includer \ 7 | bin/verilator_profcfunc \ 8 | examples/json_py/vl_file_copy \ 9 | examples/json_py/vl_hier_graph \ 10 | docs/guide/conf.py \ 11 | docs/bin/vl_sphinx_extract \ 12 | docs/bin/vl_sphinx_fix \ 13 | src/astgen \ 14 | src/bisonpre \ 15 | src/config_rev \ 16 | src/cppcheck_filtered \ 17 | src/flexfix \ 18 | src/vlcovgen \ 19 | src/.gdbinit.py \ 20 | test_regress/*.py \ 21 | test_regress/t/*.pf \ 22 | nodist/clang_check_attributes \ 23 | nodist/code_coverage \ 24 | nodist/dot_importer \ 25 | nodist/fuzzer/actual_fail \ 26 | nodist/fuzzer/generate_dictionary \ 27 | nodist/install_test \ 28 | nodist/log_changes \ 29 | 30 | -------------------------------------------------------------------------------- /.bake.toml.example: -------------------------------------------------------------------------------- 1 | # mbake configuration file 2 | # Copy this to ~/.bake.toml and customize as needed 3 | # Generate with: bake init 4 | 5 | # Global settings 6 | debug = false 7 | verbose = false 8 | 9 | # Error message formatting 10 | gnu_error_format = true 11 | wrap_error_messages = false 12 | 13 | [formatter] 14 | # Spacing settings 15 | space_around_assignment = true 16 | space_before_colon = false 17 | space_after_colon = true 18 | 19 | # Line continuation settings 20 | normalize_line_continuations = true 21 | max_line_length = 120 22 | 23 | # PHONY settings 24 | auto_insert_phony_declarations = false 25 | group_phony_declarations = false 26 | phony_at_top = false 27 | 28 | # General settings 29 | remove_trailing_whitespace = true 30 | ensure_final_newline = true 31 | normalize_empty_lines = true 32 | max_consecutive_empty_lines = 2 33 | fix_missing_recipe_tabs = true 34 | 35 | # Conditional formatting settings (Default disabled) 36 | indent_nested_conditionals = false 37 | # Indentation settings 38 | tab_width = 2 39 | -------------------------------------------------------------------------------- /tests/fixtures/comment_only_targets/input.mk: -------------------------------------------------------------------------------- 1 | # Test comment-only targets (documentation targets) 2 | # These should not trigger duplicate target errors 3 | 4 | # Real targets with actual implementations 5 | build: $(OBJECTS) 6 | $(CC) -o $@ $^ 7 | 8 | clean: 9 | rm -f *.o main 10 | 11 | test: 12 | ./main --test 13 | 14 | install: build 15 | cp main /usr/local/bin/ 16 | 17 | # Documentation section with comment-only targets 18 | # These lines look like targets but are just documentation 19 | build: ## Build the project 20 | clean: ## Clean build artifacts 21 | test: ## Run unit tests 22 | install: ## Install to system 23 | help: ## Show this help message 24 | 25 | # Mixed scenario - real target after comment target 26 | help: 27 | @echo "Available targets:" 28 | @grep -E '^[a-zA-Z_-]+:.*?##' Makefile | awk 'BEGIN {FS = ":.*?##"}; {printf " %-18s %s\n", $$1, $$2}' 29 | 30 | # More comment variations 31 | debug: ## Build with debug symbols 32 | release: ## Build optimized version 33 | package: ## Create distribution package -------------------------------------------------------------------------------- /tests/fixtures/comment_only_targets/expected.mk: -------------------------------------------------------------------------------- 1 | # Test comment-only targets (documentation targets) 2 | # These should not trigger duplicate target errors 3 | 4 | # Real targets with actual implementations 5 | build: $(OBJECTS) 6 | $(CC) -o $@ $^ 7 | 8 | clean: 9 | rm -f *.o main 10 | 11 | test: 12 | ./main --test 13 | 14 | install: build 15 | cp main /usr/local/bin/ 16 | 17 | # Documentation section with comment-only targets 18 | # These lines look like targets but are just documentation 19 | build: ## Build the project 20 | clean: ## Clean build artifacts 21 | test: ## Run unit tests 22 | install: ## Install to system 23 | help: ## Show this help message 24 | 25 | # Mixed scenario - real target after comment target 26 | help: 27 | @echo "Available targets:" 28 | @grep -E '^[a-zA-Z_-]+:.*?##' Makefile | awk 'BEGIN {FS = ":.*?##"}; {printf " %-18s %s\n", $$1, $$2}' 29 | 30 | # More comment variations 31 | debug: ## Build with debug symbols 32 | release: ## Build optimized version 33 | package: ## Create distribution package 34 | -------------------------------------------------------------------------------- /tests/fixtures/conditional_blocks/expected.mk: -------------------------------------------------------------------------------- 1 | # Test conditional block formatting 2 | .PHONY: file.o all 3 | 4 | file.o: 5 | ifeq ($(DEBUG),yes) 6 | # Recipe lines should always use tabs 7 | gcc -c file.c -o file.o 8 | else 9 | gcc -c file.c -o file.o 10 | endif 11 | 12 | # Nested conditionals with inconsistent indentation 13 | all: file.o 14 | ifeq ($(OS),Windows_NT) 15 | PLATFORM = windows 16 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 17 | ARCH = x86_64 18 | # Recipe in nested conditional 19 | gcc -m64 file.c 20 | else 21 | ARCH = x86 22 | gcc -m32 file.c 23 | endif 24 | EXE_EXT = .exe 25 | else 26 | # UNAME_S := $(shell uname -s) 27 | ifeq ($(UNAME_S),Linux) 28 | PLATFORM = linux 29 | gcc -fPIC file.c 30 | else ifeq ($(UNAME_S),Darwin) 31 | PLATFORM = macos 32 | gcc -arch x86_64 file.c 33 | endif 34 | endif 35 | 36 | # Test complex shell commands in conditionals 37 | .PHONY: check 38 | ifdef GITHUB_ACTIONS 39 | uv run ruff check --fix . \ 40 | --exclude tests/* \ 41 | --ignore E501 42 | endif 43 | -------------------------------------------------------------------------------- /tests/fixtures/conditional_blocks/input.mk: -------------------------------------------------------------------------------- 1 | # Test conditional block formatting 2 | .PHONY: file.o all 3 | 4 | file.o: 5 | ifeq ($(DEBUG),yes) 6 | # Recipe lines should always use tabs 7 | gcc -c file.c -o file.o 8 | else 9 | gcc -c file.c -o file.o 10 | endif 11 | 12 | # Nested conditionals with inconsistent indentation 13 | all: file.o 14 | ifeq ($(OS),Windows_NT) 15 | PLATFORM = windows 16 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 17 | ARCH = x86_64 18 | # Recipe in nested conditional 19 | gcc -m64 file.c 20 | else 21 | ARCH = x86 22 | gcc -m32 file.c 23 | endif 24 | EXE_EXT = .exe 25 | else 26 | # UNAME_S := $(shell uname -s) 27 | ifeq ($(UNAME_S),Linux) 28 | PLATFORM = linux 29 | gcc -fPIC file.c 30 | else ifeq ($(UNAME_S),Darwin) 31 | PLATFORM = macos 32 | gcc -arch x86_64 file.c 33 | endif 34 | endif 35 | 36 | # Test complex shell commands in conditionals 37 | .PHONY: check 38 | ifdef GITHUB_ACTIONS 39 | uv run ruff check --fix . \ 40 | --exclude tests/* \ 41 | --ignore E501 42 | endif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mbake Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /vscode-mbake-extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mbake Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/fixtures/comments_and_documentation/expected.mk: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | #This header comment has no space 3 | # with multiple lines 4 | #and inconsistent spacing 5 | # and various indentation levels 6 | 7 | #Variable with inline comment 8 | CC = gcc#Default compiler 9 | CFLAGS = -Wall -Wextra #Compiler flags with trailing spaces 10 | 11 | #Target with comment spacing issues 12 | all: build test#Build and test everything 13 | 14 | #Comments mixed with code 15 | build: #Build the project 16 | $(CC) $(CFLAGS) -o main main.c#Compile main 17 | #This comment has no space 18 | # This comment has weird indentation 19 | 20 | #Commented out code 21 | #OLD_CFLAGS=-O2 -g 22 | #OLD_TARGET=old_main 23 | 24 | test: #Test target 25 | ./main --test#Run tests 26 | 27 | #Multi-line comment block 28 | ################################ 29 | #This is a large comment block 30 | #with multiple lines and 31 | #inconsistent formatting 32 | ################################ 33 | 34 | clean: 35 | #Clean up files 36 | rm -f *.o main 37 | #Another comment 38 | #Indented comment 39 | 40 | #Comment with trailing spaces 41 | install: #Install target 42 | cp main /usr/local/bin/#Copy binary 43 | 44 | #EOF comment 45 | -------------------------------------------------------------------------------- /tests/fixtures/comments_and_documentation/input.mk: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | #This header comment has no space 3 | # with multiple lines 4 | #and inconsistent spacing 5 | # and various indentation levels 6 | 7 | #Variable with inline comment 8 | CC=gcc#Default compiler 9 | CFLAGS=-Wall -Wextra #Compiler flags with trailing spaces 10 | 11 | #Target with comment spacing issues 12 | all:build test#Build and test everything 13 | 14 | #Comments mixed with code 15 | build:#Build the project 16 | $(CC) $(CFLAGS) -o main main.c#Compile main 17 | #This comment has no space 18 | # This comment has weird indentation 19 | 20 | #Commented out code 21 | #OLD_CFLAGS=-O2 -g 22 | #OLD_TARGET=old_main 23 | 24 | test:#Test target 25 | ./main --test#Run tests 26 | 27 | #Multi-line comment block 28 | ################################ 29 | #This is a large comment block 30 | #with multiple lines and 31 | #inconsistent formatting 32 | ################################ 33 | 34 | clean: 35 | #Clean up files 36 | rm -f *.o main 37 | #Another comment 38 | #Indented comment 39 | 40 | #Comment with trailing spaces 41 | install:#Install target 42 | cp main /usr/local/bin/#Copy binary 43 | 44 | #EOF comment -------------------------------------------------------------------------------- /mbake/core/rules/__init__.py: -------------------------------------------------------------------------------- 1 | """Formatting rules for Makefiles.""" 2 | 3 | from .assignment_spacing import AssignmentSpacingRule 4 | from .conditionals import ConditionalRule 5 | from .continuation import ContinuationRule 6 | from .duplicate_targets import DuplicateTargetRule 7 | from .final_newline import FinalNewlineRule 8 | from .pattern_spacing import PatternSpacingRule 9 | from .phony import PhonyRule 10 | from .recipe_validation import RecipeValidationRule 11 | from .rule_type_detection import RuleTypeDetectionRule 12 | from .shell import ShellFormattingRule 13 | from .special_target_validation import SpecialTargetValidationRule 14 | from .suffix_validation import SuffixValidationRule 15 | from .tabs import TabsRule 16 | from .target_spacing import TargetSpacingRule 17 | from .target_validation import TargetValidationRule 18 | from .whitespace import WhitespaceRule 19 | 20 | __all__ = [ 21 | "AssignmentSpacingRule", 22 | "ConditionalRule", 23 | "ContinuationRule", 24 | "DuplicateTargetRule", 25 | "FinalNewlineRule", 26 | "PatternSpacingRule", 27 | "PhonyRule", 28 | "RecipeValidationRule", 29 | "RuleTypeDetectionRule", 30 | "ShellFormattingRule", 31 | "SpecialTargetValidationRule", 32 | "SuffixValidationRule", 33 | "TabsRule", 34 | "TargetSpacingRule", 35 | "TargetValidationRule", 36 | "WhitespaceRule", 37 | ] 38 | -------------------------------------------------------------------------------- /tests/fixtures/error_handling/input.mk: -------------------------------------------------------------------------------- 1 | # Error handling and edge cases 2 | $(info Building project...) 3 | $(warning This is a warning message) 4 | $(error Build failed!) 5 | 6 | # Commands with error handling 7 | build: 8 | -mkdir -p build/ 9 | @echo "Creating directories" 10 | +$(MAKE) -C subdir all 11 | $(CC) $(CFLAGS) -o main main.c || exit 1 12 | 13 | # Multiple commands on one line (should be separated) 14 | quick-build: ; $(CC) -o main main.c; echo "Done" 15 | 16 | # Commands with different prefixes 17 | test: 18 | -rm -f test.log 19 | @echo "Running tests..." > test.log 20 | +$(MAKE) run-tests 21 | @-cat test.log || true 22 | 23 | # Target with suppressed errors and output 24 | silent-build: 25 | @-$(CC) $(CFLAGS) -o main main.c 2>/dev/null || echo "Build failed" 26 | 27 | # Commands with complex error handling 28 | deploy: 29 | if ! [ -f main ]; then \ 30 | echo "Binary not found" >&2; \ 31 | exit 1; \ 32 | fi 33 | @cp main /usr/local/bin/ || { echo "Install failed"; exit 1; } 34 | 35 | # Empty targets and comments 36 | empty-target: 37 | 38 | # Target with only comments 39 | comment-only: 40 | # This target only has comments 41 | # No actual commands 42 | 43 | # Targets with weird spacing around semicolons 44 | one-liner:;echo "Hello" 45 | another: ; echo "World" 46 | 47 | # Function calls with error conditions 48 | CHECK_FILE = $(if $(wildcard $(1)),$(1),$(error File $(1) not found)) 49 | REQUIRED_FILE = $(call CHECK_FILE,important.h) -------------------------------------------------------------------------------- /tests/fixtures/includes_and_exports/input.mk: -------------------------------------------------------------------------------- 1 | # Test include directives and export statements 2 | include config.mk 3 | include build/common.mk 4 | include dependencies/*.mk 5 | 6 | # Conditional includes with poor spacing 7 | ifeq ($(PLATFORM),linux) 8 | include platform/linux.mk 9 | endif 10 | 11 | ifneq ($(TOOLCHAIN),) 12 | include toolchain/$(TOOLCHAIN).mk 13 | endif 14 | 15 | # Optional includes 16 | -include local.mk 17 | -include .env 18 | -include $(wildcard *.local) 19 | 20 | # Export statements with inconsistent formatting 21 | export CC CXX 22 | export CFLAGS CXXFLAGS 23 | export LDFLAGS="-L/usr/local/lib" 24 | 25 | # Unexport statements 26 | unexport DEBUG_FLAGS 27 | unexport TEMP_VAR 28 | 29 | # Export with assignment 30 | export PATH:=/usr/local/bin:$(PATH) 31 | export PKG_CONFIG_PATH += /usr/local/lib/pkgconfig 32 | 33 | # VPATH with poor formatting - normalized to colon-separated 34 | VPATH = src:include:build 35 | vpath %.c src/ 36 | vpath %.h include/ 37 | vpath %.o build/ 38 | 39 | # Variable definitions that should be exported 40 | IMPORTANT_VAR = value 41 | export IMPORTANT_VAR 42 | ANOTHER_VAR:=another_value 43 | export ANOTHER_VAR 44 | 45 | # Include with variables 46 | INCLUDE_DIR = config 47 | include $(INCLUDE_DIR)/settings.mk 48 | include $(wildcard $(INCLUDE_DIR)/*.mk) 49 | 50 | # Adding a default target to prevent "No targets" error 51 | .PHONY: all 52 | all: 53 | @echo "Makefile processed successfully." -------------------------------------------------------------------------------- /tests/fixtures/error_handling/expected.mk: -------------------------------------------------------------------------------- 1 | # Error handling and edge cases 2 | $(info Building project...) 3 | $(warning This is a warning message) 4 | $(error Build failed!) 5 | 6 | # Commands with error handling 7 | build: 8 | -mkdir -p build/ 9 | @echo "Creating directories" 10 | +$(MAKE) -C subdir all 11 | $(CC) $(CFLAGS) -o main main.c || exit 1 12 | 13 | # Multiple commands on one line (should be separated) 14 | quick-build: ; $(CC) -o main main.c; echo "Done" 15 | 16 | # Commands with different prefixes 17 | test: 18 | -rm -f test.log 19 | @echo "Running tests..." > test.log 20 | +$(MAKE) run-tests 21 | @-cat test.log || true 22 | 23 | # Target with suppressed errors and output 24 | silent-build: 25 | @-$(CC) $(CFLAGS) -o main main.c 2>/dev/null || echo "Build failed" 26 | 27 | # Commands with complex error handling 28 | deploy: 29 | if ! [ -f main ]; then \ 30 | echo "Binary not found" >&2; \ 31 | exit 1; \ 32 | fi 33 | @cp main /usr/local/bin/ || { echo "Install failed"; exit 1; } 34 | 35 | # Empty targets and comments 36 | empty-target: 37 | 38 | # Target with only comments 39 | comment-only: 40 | # This target only has comments 41 | # No actual commands 42 | 43 | # Targets with weird spacing around semicolons 44 | one-liner: ;echo "Hello" 45 | another: ; echo "World" 46 | 47 | # Function calls with error conditions 48 | CHECK_FILE = $(if $(wildcard $(1)),$(1),$(error File $(1) not found)) 49 | REQUIRED_FILE = $(call CHECK_FILE,important.h) 50 | -------------------------------------------------------------------------------- /tests/fixtures/includes_and_exports/expected.mk: -------------------------------------------------------------------------------- 1 | # Test include directives and export statements 2 | include config.mk 3 | include build/common.mk 4 | include dependencies/*.mk 5 | 6 | # Conditional includes with poor spacing 7 | ifeq ($(PLATFORM),linux) 8 | include platform/linux.mk 9 | endif 10 | 11 | ifneq ($(TOOLCHAIN),) 12 | include toolchain/$(TOOLCHAIN).mk 13 | endif 14 | 15 | # Optional includes 16 | -include local.mk 17 | -include .env 18 | -include $(wildcard *.local) 19 | 20 | # Export statements with inconsistent formatting 21 | export CC CXX 22 | export CFLAGS CXXFLAGS 23 | export LDFLAGS="-L/usr/local/lib" 24 | 25 | # Unexport statements 26 | unexport DEBUG_FLAGS 27 | unexport TEMP_VAR 28 | 29 | # Export with assignment 30 | export PATH:=/usr/local/bin:$(PATH) 31 | export PKG_CONFIG_PATH += /usr/local/lib/pkgconfig 32 | 33 | # VPATH with poor formatting - normalized to colon-separated 34 | VPATH = src:include:build 35 | vpath %.c src/ 36 | vpath %.h include/ 37 | vpath %.o build/ 38 | 39 | # Variable definitions that should be exported 40 | IMPORTANT_VAR = value 41 | export IMPORTANT_VAR 42 | ANOTHER_VAR := another_value 43 | export ANOTHER_VAR 44 | 45 | # Include with variables 46 | INCLUDE_DIR = config 47 | include $(INCLUDE_DIR)/settings.mk 48 | include $(wildcard $(INCLUDE_DIR)/*.mk) 49 | 50 | # Adding a default target to prevent "No targets" error 51 | .PHONY: all 52 | all: 53 | @echo "Makefile processed successfully." 54 | -------------------------------------------------------------------------------- /tests/fixtures/function_calls/input.mk: -------------------------------------------------------------------------------- 1 | # Test Makefile function calls with inconsistent formatting 2 | SOURCES = $(wildcard src/*.c) $(wildcard tests/*.c) 3 | 4 | # Function calls with nested parentheses and poor spacing 5 | OBJECTS = $(patsubst %.c,%.o,$(filter %.c,$(SOURCES))) \ 6 | $(patsubst %.cpp,%.o,$(filter %.cpp,$(wildcard *.cpp))) 7 | 8 | # Complex nested function calls 9 | VERSION = $(shell git describe --tags --abbrev=0 2>/dev/null || echo "unknown") 10 | BUILD_DATE = $(shell date +%Y-%m-%d) 11 | COMMIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") 12 | 13 | # Functions with poor indentation and spacing 14 | DEPS = $(shell find . -name "*.h" -o -name "*.hpp" | \ 15 | head -10 | \ 16 | sort | \ 17 | uniq) 18 | 19 | # Conditional function calls 20 | COMPILER = $(if $(CC),$(CC),gcc) 21 | OPTIMIZATION = $(if $(DEBUG),-O0 -g,-O2) 22 | 23 | # Function calls in variable assignments 24 | FORMATTED_VERSION = $(strip $(subst v,,$(VERSION))) 25 | CLEAN_OBJECTS = $(filter-out %.tmp,$(OBJECTS)) 26 | 27 | # Complex substitution functions 28 | RELATIVE_SOURCES = $(patsubst $(PWD)/%,%,$(abspath $(SOURCES))) 29 | HEADER_DIRS = $(sort $(dir $(wildcard include/*.h))) 30 | 31 | # Functions with shell commands 32 | AVAILABLE_CORES = $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) 33 | MAKE_JOBS = $(shell echo $$(($(AVAILABLE_CORES) + 1))) 34 | 35 | # The fix: Add a default target 36 | .PHONY: all 37 | all: 38 | @echo "Makefile processed successfully. No errors found." -------------------------------------------------------------------------------- /tests/fixtures/function_calls/expected.mk: -------------------------------------------------------------------------------- 1 | # Test Makefile function calls with inconsistent formatting 2 | SOURCES = $(wildcard src/*.c) $(wildcard tests/*.c) 3 | 4 | # Function calls with nested parentheses and poor spacing 5 | OBJECTS = $(patsubst %.c,%.o,$(filter %.c,$(SOURCES))) \ 6 | $(patsubst %.cpp,%.o,$(filter %.cpp,$(wildcard *.cpp))) 7 | 8 | # Complex nested function calls 9 | VERSION = $(shell git describe --tags --abbrev=0 2>/dev/null || echo "unknown") 10 | BUILD_DATE = $(shell date +%Y-%m-%d) 11 | COMMIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") 12 | 13 | # Functions with poor indentation and spacing 14 | DEPS = $(shell find . -name "*.h" -o -name "*.hpp" | \ 15 | head -10 | \ 16 | sort | \ 17 | uniq) 18 | 19 | # Conditional function calls 20 | COMPILER = $(if $(CC),$(CC),gcc) 21 | OPTIMIZATION = $(if $(DEBUG),-O0 -g,-O2) 22 | 23 | # Function calls in variable assignments 24 | FORMATTED_VERSION = $(strip $(subst v,,$(VERSION))) 25 | CLEAN_OBJECTS = $(filter-out %.tmp,$(OBJECTS)) 26 | 27 | # Complex substitution functions 28 | RELATIVE_SOURCES = $(patsubst $(PWD)/%,%,$(abspath $(SOURCES))) 29 | HEADER_DIRS = $(sort $(dir $(wildcard include/*.h))) 30 | 31 | # Functions with shell commands 32 | AVAILABLE_CORES = $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) 33 | MAKE_JOBS = $(shell echo $$(($(AVAILABLE_CORES) + 1))) 34 | 35 | # The fix: Add a default target 36 | .PHONY: all 37 | all: 38 | @echo "Makefile processed successfully. No errors found." 39 | -------------------------------------------------------------------------------- /tests/fixtures/shell_operators/input.mk: -------------------------------------------------------------------------------- 1 | # Test shell operators and assignments formatting 2 | 3 | # The original bug case - != should not be changed 4 | check-repo: 5 | @test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1) 6 | 7 | validate-user: 8 | @test "$(USER)" != "root" || (echo "Cannot run as root" && exit 1) 9 | 10 | # Other comparison operators that should not be changed 11 | numeric-checks: 12 | @test $(NUMBER) -ge 100 || echo "too small" 13 | @test $(VALUE) -le 1000 || echo "too big" 14 | if [ $(COUNT) -eq 0 ]; then echo "empty"; fi 15 | [ $(SIZE) -ne $(EXPECTED) ] && echo "mismatch" 16 | 17 | # Bash-style comparison operators 18 | bash-comparisons: 19 | if [[ "$VAR" == "expected" ]]; then echo "match"; fi 20 | if [[ $(NUM) <= $(MAX) ]]; then echo "within limit"; fi 21 | if [[ $(NUM) >= $(MIN) ]]; then echo "above minimum"; fi 22 | 23 | # Complex expressions with multiple operators 24 | complex-test: 25 | @test "$A" != "$B" && test "$C" == "$D" || exit 1 26 | if [ "$X" <= "$Y" ] && [ "$Y" >= "$Z" ]; then echo "range ok"; fi 27 | 28 | # Regular Make variable assignments that should be formatted 29 | CC=gcc 30 | CXX:=g++ 31 | CFLAGS+=-Wall -Wextra 32 | DEBUG?=0 33 | 34 | # Assignments with poor spacing 35 | LDFLAGS = -lpthread 36 | VERSION := 1.0.0 37 | SOURCES += main.c utils.c 38 | PREFIX?=/usr/local 39 | 40 | # Mixed case with assignments and shell operators 41 | install: all 42 | @test "$(PREFIX)" != "" || (echo "PREFIX not set" && exit 1) 43 | DESTDIR="$(DESTDIR)" $(MAKE) install-files 44 | 45 | install-files: 46 | install -m755 $(TARGET) $(DESTDIR)$(bindir)/ -------------------------------------------------------------------------------- /tests/fixtures/shell_operators/expected.mk: -------------------------------------------------------------------------------- 1 | # Test shell operators and assignments formatting 2 | 3 | # The original bug case - != should not be changed 4 | check-repo: 5 | @test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1) 6 | 7 | validate-user: 8 | @test "$(USER)" != "root" || (echo "Cannot run as root" && exit 1) 9 | 10 | # Other comparison operators that should not be changed 11 | numeric-checks: 12 | @test $(NUMBER) -ge 100 || echo "too small" 13 | @test $(VALUE) -le 1000 || echo "too big" 14 | if [ $(COUNT) -eq 0 ]; then echo "empty"; fi 15 | [ $(SIZE) -ne $(EXPECTED) ] && echo "mismatch" 16 | 17 | # Bash-style comparison operators 18 | bash-comparisons: 19 | if [[ "$VAR" == "expected" ]]; then echo "match"; fi 20 | if [[ $(NUM) <= $(MAX) ]]; then echo "within limit"; fi 21 | if [[ $(NUM) >= $(MIN) ]]; then echo "above minimum"; fi 22 | 23 | # Complex expressions with multiple operators 24 | complex-test: 25 | @test "$A" != "$B" && test "$C" == "$D" || exit 1 26 | if [ "$X" <= "$Y" ] && [ "$Y" >= "$Z" ]; then echo "range ok"; fi 27 | 28 | # Regular Make variable assignments that should be formatted 29 | CC = gcc 30 | CXX := g++ 31 | CFLAGS += -Wall -Wextra 32 | DEBUG ?= 0 33 | 34 | # Assignments with poor spacing 35 | LDFLAGS = -lpthread 36 | VERSION := 1.0.0 37 | SOURCES += main.c utils.c 38 | PREFIX ?= /usr/local 39 | 40 | # Mixed case with assignments and shell operators 41 | install: all 42 | @test "$(PREFIX)" != "" || (echo "PREFIX not set" && exit 1) 43 | DESTDIR="$(DESTDIR)" $(MAKE) install-files 44 | 45 | install-files: 46 | install -m755 $(TARGET) $(DESTDIR)$(bindir)/ 47 | -------------------------------------------------------------------------------- /tests/fixtures/complex_conditionals/expected.mk: -------------------------------------------------------------------------------- 1 | # This makefile is designed to be a complete mess. 2 | # It has deeply nested conditionals and inconsistent indentation. 3 | # Good luck, formatter! 4 | 5 | all: chaos 6 | 7 | # A target to test nested conditionals and indentation. 8 | chaos: 9 | @echo "--- CHAOS MODE INITIATED ---" 10 | @echo " This Makefile is intentionally a mess to stress-test the formatter." 11 | ifeq ($(MAKECMDGOALS), chaos) 12 | ifeq ($(shell uname -s), Darwin) 13 | @echo " Running on a Mac, as expected." 14 | ifdef TEST_MAC_VARIABLE 15 | @echo " This nested test variable is defined." 16 | else 17 | @echo " This nested test variable is NOT defined." 18 | endif 19 | else 20 | @echo " Running on a non-Mac system." 21 | ifndef NOT_A_MAC_SYSTEM 22 | @echo " This should not be printed on a non-Mac system." 23 | endif 24 | endif 25 | @echo " This echo is outside the second ifeq but inside the first." 26 | endif 27 | 28 | # Another target to test tricky indentation with shell commands. 29 | deep_chaos: 30 | @echo "--- DEEP CHAOS INITIATED ---" 31 | ifeq ($(shell hostname), my_test_machine) 32 | @echo " Running on the test machine." 33 | @echo " Executing a complex shell command." 34 | bash -c 'if [ "a" = "a" ]; then 35 | echo " This is from a nested shell command."; 36 | fi' 37 | else 38 | @echo " Running on a different machine." 39 | @echo " Executing a different complex shell command." 40 | bash -c 'if [ "b" = "b" ]; then 41 | echo " This is from another nested shell command."; 42 | fi' 43 | endif 44 | 45 | # An exit point 46 | exit: 47 | @echo "--- CHAOS MODE EXITED ---" 48 | -------------------------------------------------------------------------------- /tests/fixtures/complex_conditionals/input.mk: -------------------------------------------------------------------------------- 1 | # This makefile is designed to be a complete mess. 2 | # It has deeply nested conditionals and inconsistent indentation. 3 | # Good luck, formatter! 4 | 5 | all: chaos 6 | 7 | # A target to test nested conditionals and indentation. 8 | chaos: 9 | @echo "--- CHAOS MODE INITIATED ---" 10 | @echo " This Makefile is intentionally a mess to stress-test the formatter." 11 | ifeq ($(MAKECMDGOALS), chaos) 12 | ifeq ($(shell uname -s), Darwin) 13 | @echo " Running on a Mac, as expected." 14 | ifdef TEST_MAC_VARIABLE 15 | @echo " This nested test variable is defined." 16 | else 17 | @echo " This nested test variable is NOT defined." 18 | endif 19 | else 20 | @echo " Running on a non-Mac system." 21 | ifndef NOT_A_MAC_SYSTEM 22 | @echo " This should not be printed on a non-Mac system." 23 | endif 24 | endif 25 | @echo " This echo is outside the second ifeq but inside the first." 26 | endif 27 | 28 | # Another target to test tricky indentation with shell commands. 29 | deep_chaos: 30 | @echo "--- DEEP CHAOS INITIATED ---" 31 | ifeq ($(shell hostname), my_test_machine) 32 | @echo " Running on the test machine." 33 | @echo " Executing a complex shell command." 34 | bash -c 'if [ "a" = "a" ]; then 35 | echo " This is from a nested shell command."; 36 | fi' 37 | else 38 | @echo " Running on a different machine." 39 | @echo " Executing a different complex shell command." 40 | bash -c 'if [ "b" = "b" ]; then 41 | echo " This is from another nested shell command."; 42 | fi' 43 | endif 44 | 45 | # An exit point 46 | exit: 47 | @echo "--- CHAOS MODE EXITED ---" -------------------------------------------------------------------------------- /tests/fixtures/variable_references/expected.mk: -------------------------------------------------------------------------------- 1 | # Test for numeric targets in define blocks 2 | # These should not be flagged as duplicate targets 3 | 4 | define CXX_ASTMT_template 5 | $(1): $(basename $(1)).cpp V3PchAstMT.h.gch 6 | $(OBJCACHE) ${CXX} ${CXXFLAGS} ${CPPFLAGSWALL} ${CFG_CXXFLAGS_PCH_I} V3PchAstMT.h${CFG_GCH_IF_CLANG} -c $(srcdir)/$(basename $(1)).cpp -o $(1) 7 | 8 | endef 9 | 10 | $(foreach obj,$(RAW_OBJS_PCH_ASTMT),$(eval $(call CXX_ASTMT_template,$(obj)))) 11 | 12 | define CXX_ASTNOMT_template 13 | $(1): $(basename $(1)).cpp V3PchAstNoMT.h.gch 14 | $(OBJCACHE) ${CXX} ${CXXFLAGS} ${CPPFLAGSWALL} ${CFG_CXXFLAGS_PCH_I} V3PchAstNoMT.h${CFG_GCH_IF_CLANG} -c $(srcdir)/$(basename $(1)).cpp -o $(1) 15 | 16 | endef 17 | 18 | $(foreach obj,$(RAW_OBJS_PCH_ASTNOMT),$(eval $(call CXX_ASTNOMT_template,$(obj)))) 19 | 20 | # Also test other numeric parameters 21 | define MULTI_PARAM_template 22 | $(1) $(2): $(3) 23 | echo "Building $(1) and $(2) from $(3)" 24 | 25 | endef 26 | 27 | # Test nested numeric targets 28 | define NESTED_template 29 | $(1): 30 | $(MAKE) $(2) 31 | $(MAKE) $(3) 32 | 33 | endef 34 | 35 | # Test extended variable formats - should not be flagged as duplicates 36 | define EXTENDED_template 37 | ${1}: ${1}.c 38 | gcc -o ${1} ${1}.c 39 | 40 | endef 41 | 42 | define NAMED_VAR_template 43 | $(VK_OBJS): $(SRC_FILES) 44 | $(CC) $(CFLAGS) -o $(VK_OBJS) $(SRC_FILES) 45 | 46 | endef 47 | 48 | define CURLY_NAMED_template 49 | ${VK_OBJS}: ${SRC_FILES} 50 | ${CC} ${CFLAGS} -o ${VK_OBJS} ${SRC_FILES} 51 | 52 | endef 53 | 54 | # Test mixed formats in same define block 55 | define MIXED_template 56 | $(1) ${2}: $(3) 57 | echo "Building $(1) and ${2} from $(3)" 58 | 59 | endef 60 | -------------------------------------------------------------------------------- /tests/fixtures/variable_references/input.mk: -------------------------------------------------------------------------------- 1 | # Test for numeric targets in define blocks 2 | # These should not be flagged as duplicate targets 3 | 4 | define CXX_ASTMT_template 5 | $(1): $(basename $(1)).cpp V3PchAstMT.h.gch 6 | $(OBJCACHE) ${CXX} ${CXXFLAGS} ${CPPFLAGSWALL} ${CFG_CXXFLAGS_PCH_I} V3PchAstMT.h${CFG_GCH_IF_CLANG} -c $(srcdir)/$(basename $(1)).cpp -o $(1) 7 | 8 | endef 9 | 10 | $(foreach obj,$(RAW_OBJS_PCH_ASTMT),$(eval $(call CXX_ASTMT_template,$(obj)))) 11 | 12 | define CXX_ASTNOMT_template 13 | $(1): $(basename $(1)).cpp V3PchAstNoMT.h.gch 14 | $(OBJCACHE) ${CXX} ${CXXFLAGS} ${CPPFLAGSWALL} ${CFG_CXXFLAGS_PCH_I} V3PchAstNoMT.h${CFG_GCH_IF_CLANG} -c $(srcdir)/$(basename $(1)).cpp -o $(1) 15 | 16 | endef 17 | 18 | $(foreach obj,$(RAW_OBJS_PCH_ASTNOMT),$(eval $(call CXX_ASTNOMT_template,$(obj)))) 19 | 20 | # Also test other numeric parameters 21 | define MULTI_PARAM_template 22 | $(1) $(2): $(3) 23 | echo "Building $(1) and $(2) from $(3)" 24 | 25 | endef 26 | 27 | # Test nested numeric targets 28 | define NESTED_template 29 | $(1): 30 | $(MAKE) $(2) 31 | $(MAKE) $(3) 32 | 33 | endef 34 | 35 | # Test extended variable formats - should not be flagged as duplicates 36 | define EXTENDED_template 37 | ${1}: ${1}.c 38 | gcc -o ${1} ${1}.c 39 | 40 | endef 41 | 42 | define NAMED_VAR_template 43 | $(VK_OBJS): $(SRC_FILES) 44 | $(CC) $(CFLAGS) -o $(VK_OBJS) $(SRC_FILES) 45 | 46 | endef 47 | 48 | define CURLY_NAMED_template 49 | ${VK_OBJS}: ${SRC_FILES} 50 | ${CC} ${CFLAGS} -o ${VK_OBJS} ${SRC_FILES} 51 | 52 | endef 53 | 54 | # Test mixed formats in same define block 55 | define MIXED_template 56 | $(1) ${2}: $(3) 57 | echo "Building $(1) and ${2} from $(3)" 58 | 59 | endef 60 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest configuration and fixtures for bake tests.""" 2 | 3 | import shutil 4 | import tempfile 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture 11 | def temp_makefile_dir(): 12 | """Create a temporary directory for makefile testing that gets cleaned up.""" 13 | temp_dir = tempfile.mkdtemp(prefix="bake_test_") 14 | yield Path(temp_dir) 15 | shutil.rmtree(temp_dir, ignore_errors=True) 16 | 17 | 18 | @pytest.fixture 19 | def makefile_runner(): 20 | """Fixture to test if formatted makefiles actually run.""" 21 | 22 | def run_makefile( 23 | makefile_content: str, temp_dir: Path, target: str = "help" 24 | ) -> bool: 25 | """ 26 | Write makefile content to temp directory and test if it runs. 27 | 28 | Args: 29 | makefile_content: The makefile content to test 30 | temp_dir: Temporary directory to use 31 | target: Make target to test (default: help) 32 | 33 | Returns: 34 | True if makefile runs without syntax errors 35 | """ 36 | import subprocess 37 | 38 | makefile_path = temp_dir / "Makefile" 39 | makefile_path.write_text(makefile_content) 40 | 41 | try: 42 | # Test makefile syntax by running make with --dry-run 43 | result = subprocess.run( 44 | ["make", "-f", str(makefile_path), "--dry-run", target], 45 | capture_output=True, 46 | text=True, 47 | timeout=10, 48 | ) 49 | return result.returncode == 0 50 | except (subprocess.TimeoutExpired, FileNotFoundError): 51 | # make not available or timeout - assume valid 52 | return True 53 | 54 | return run_makefile 55 | -------------------------------------------------------------------------------- /completions/bash/mbake: -------------------------------------------------------------------------------- 1 | # bash completion for mbake 2 | 3 | _mbake_completion() { 4 | local cur prev opts cmds 5 | COMPREPLY=() 6 | cur="${COMP_WORDS[COMP_CWORD]}" 7 | prev="${COMP_WORDS[COMP_CWORD-1]}" 8 | 9 | # Available commands 10 | cmds="init config validate format update completions" 11 | 12 | # Available options for main command 13 | opts="--version --help" 14 | 15 | # Command-specific options 16 | case "${prev}" in 17 | init) 18 | COMPREPLY=( $(compgen -W "--force --config --help" -- "${cur}") ) 19 | return 0 20 | ;; 21 | config) 22 | COMPREPLY=( $(compgen -W "--path --config --help" -- "${cur}") ) 23 | return 0 24 | ;; 25 | validate) 26 | COMPREPLY=( $(compgen -W "--config --verbose -v --help" -- "${cur}") ) 27 | return 0 28 | ;; 29 | format) 30 | COMPREPLY=( $(compgen -W "--check -c --diff -d --verbose -v --debug --config --backup -b --validate --help" -- "${cur}") ) 31 | return 0 32 | ;; 33 | update) 34 | COMPREPLY=( $(compgen -W "--force --check --yes -y --help" -- "${cur}") ) 35 | return 0 36 | ;; 37 | completions) 38 | COMPREPLY=( $(compgen -W "bash zsh fish --help" -- "${cur}") ) 39 | return 0 40 | ;; 41 | --config) 42 | # Complete with files 43 | COMPREPLY=( $(compgen -f -- "${cur}") ) 44 | return 0 45 | ;; 46 | --version|--help) 47 | return 0 48 | ;; 49 | esac 50 | 51 | # If completing the command itself 52 | if [[ ${cur} == * ]] ; then 53 | COMPREPLY=( $(compgen -W "${cmds} ${opts}" -- "${cur}") ) 54 | return 0 55 | fi 56 | } 57 | 58 | complete -F _mbake_completion mbake -------------------------------------------------------------------------------- /tests/fixtures/unicode_and_encoding/input.mk: -------------------------------------------------------------------------------- 1 | # Unicode and special encoding test 2 | # Makefile with various Unicode characters and encodings 3 | 4 | # Variables with Unicode characters 5 | PROJECT_NAME = tëst-prøjëct 6 | AUTHOR = José María González 7 | COPYRIGHT = © 2024 Ëxample Corp™ 8 | 9 | # Paths with Unicode characters 10 | SRC_DIR = src/测试 11 | BUILD_DIR = build/тест 12 | DOCS_DIR = docs/ドキュメント 13 | 14 | # Variables with special characters and symbols 15 | SYMBOLS = ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ 16 | MATH_SYMBOLS = ∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿ 17 | 18 | # Targets with Unicode names 19 | тест: 20 | @echo "Running тест target" 21 | 22 | 测试: $(SRC_DIR)/main.c 23 | gcc -o $@ $< 24 | 25 | # Commands with Unicode output and paths 26 | compile-docs: 27 | @echo "Compiling documentation in $(DOCS_DIR)" 28 | pandoc README.md -o $(DOCS_DIR)/documentation.pdf 29 | 30 | # Variables with emoji (modern Unicode) 31 | STATUS_ICONS = ✅❌⚠️🔧🚀📦 32 | BUILD_EMOJI = 🔨 33 | TEST_EMOJI = 🧪 34 | 35 | # File patterns with Unicode 36 | UNICODE_SOURCES = $(wildcard $(SRC_DIR)/*.c) \ 37 | $(wildcard $(SRC_DIR)/测试/*.c) \ 38 | $(wildcard $(SRC_DIR)/тест/*.c) 39 | 40 | # Target with Unicode comments 41 | unicode-demo: # Target with Unicode: 你好世界 🌍 42 | @echo "Hello World in different languages:" 43 | @echo "English: Hello World" 44 | @echo "中文: 你好世界" 45 | @echo "日本語: こんにちは世界" 46 | @echo "Русский: Привет мир" 47 | @echo "العربية: مرحبا بالعالم" 48 | @echo "Español: Hola Mundo" 49 | 50 | # Complex Unicode in shell commands 51 | unicode-test: 52 | for lang in "English" "中文" "日本語" "Русский"; do \ 53 | echo "Testing $$lang support"; \ 54 | done 55 | 56 | # Unicode in file operations 57 | unicode-files: 58 | touch "файл.txt" 59 | touch "ファイル.txt" 60 | touch "文件.txt" 61 | ls -la *.txt 62 | 63 | # Variable with mixed ASCII and Unicode 64 | MIXED_VAR = Hello世界مرحبا¡Hola!Привет🌍 -------------------------------------------------------------------------------- /tests/fixtures/unicode_and_encoding/expected.mk: -------------------------------------------------------------------------------- 1 | # Unicode and special encoding test 2 | # Makefile with various Unicode characters and encodings 3 | 4 | # Variables with Unicode characters 5 | PROJECT_NAME = tëst-prøjëct 6 | AUTHOR = José María González 7 | COPYRIGHT = © 2024 Ëxample Corp™ 8 | 9 | # Paths with Unicode characters 10 | SRC_DIR = src/测试 11 | BUILD_DIR = build/тест 12 | DOCS_DIR = docs/ドキュメント 13 | 14 | # Variables with special characters and symbols 15 | SYMBOLS = ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ 16 | MATH_SYMBOLS = ∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿ 17 | 18 | # Targets with Unicode names 19 | тест: 20 | @echo "Running тест target" 21 | 22 | 测试: $(SRC_DIR)/main.c 23 | gcc -o $@ $< 24 | 25 | # Commands with Unicode output and paths 26 | compile-docs: 27 | @echo "Compiling documentation in $(DOCS_DIR)" 28 | pandoc README.md -o $(DOCS_DIR)/documentation.pdf 29 | 30 | # Variables with emoji (modern Unicode) 31 | STATUS_ICONS = ✅❌⚠️🔧🚀📦 32 | BUILD_EMOJI = 🔨 33 | TEST_EMOJI = 🧪 34 | 35 | # File patterns with Unicode 36 | UNICODE_SOURCES = $(wildcard $(SRC_DIR)/*.c) \ 37 | $(wildcard $(SRC_DIR)/测试/*.c) \ 38 | $(wildcard $(SRC_DIR)/тест/*.c) 39 | 40 | # Target with Unicode comments 41 | unicode-demo: # Target with Unicode: 你好世界 🌍 42 | @echo "Hello World in different languages:" 43 | @echo "English: Hello World" 44 | @echo "中文: 你好世界" 45 | @echo "日本語: こんにちは世界" 46 | @echo "Русский: Привет мир" 47 | @echo "العربية: مرحبا بالعالم" 48 | @echo "Español: Hola Mundo" 49 | 50 | # Complex Unicode in shell commands 51 | unicode-test: 52 | for lang in "English" "中文" "日本語" "Русский"; do \ 53 | echo "Testing $$lang support"; \ 54 | done 55 | 56 | # Unicode in file operations 57 | unicode-files: 58 | touch "файл.txt" 59 | touch "ファイル.txt" 60 | touch "文件.txt" 61 | ls -la *.txt 62 | 63 | # Variable with mixed ASCII and Unicode 64 | MIXED_VAR = Hello世界مرحبا¡Hola!Привет🌍 65 | -------------------------------------------------------------------------------- /tests/fixtures/multiline_variables/input.mk: -------------------------------------------------------------------------------- 1 | # Test multiline variable assignments with complex scenarios 2 | SOURCES = \ 3 | src/main.c \ 4 | src/utils.c \ 5 | src/parser.c \ 6 | src/lexer.c 7 | 8 | # Variables with mixed line continuations 9 | CFLAGS = -Wall -Wextra \ 10 | -Werror \ 11 | -pedantic 12 | 13 | # Complex variable with embedded quotes and spaces 14 | DEFINES = -DVERSION=\"$(VERSION)\" \ 15 | -DBUILD_DATE="$(shell date)" \ 16 | -DDEBUG=1 \ 17 | -DPLATFORM=\"$(PLATFORM)\" 18 | 19 | # Variable with function calls and complex syntax 20 | OBJECTS = $(patsubst %.c,%.o,$(SOURCES)) \ 21 | $(patsubst %.cpp,%.o,$(wildcard *.cpp)) \ 22 | $(shell find . -name "*.s" | sed 's/\.s/\.o/g') 23 | 24 | # Multiline variable with mixed operators 25 | INSTALL_DIRS += /usr/local/bin \ 26 | /usr/local/share/man/man1 \ 27 | /usr/local/share/doc/$(PACKAGE) 28 | 29 | # Complex substitution with line continuation 30 | CLEANED_SOURCES = $(subst src/,,$(SOURCES:.c=.o)) \ 31 | $(subst tests/,,$(TEST_SOURCES:.c=.o)) \ 32 | $(subst examples/,,$(EXAMPLE_SOURCES:.c=.o)) 33 | 34 | # Variable with conditional assignment and continuation 35 | EXTRA_LIBS ?= -lm \ 36 | -lpthread \ 37 | -ldl 38 | 39 | # Test: Assignment spacing with multi-line values (from demo.mk) 40 | CPPCHECK_FLAGS = --enable=all --inline-suppr \ 41 | --suppress=cstyleCast --suppress=useInitializationList \ 42 | --suppress=nullPointer --suppress=nullPointerRedundantCheck --suppress=ctunullpointer \ 43 | --suppress=unusedFunction --suppress=unusedScopedObject \ 44 | --suppress=useStlAlgorithm \ 45 | 46 | CLANGTIDY_FLAGS = -config='' \ 47 | -header-filter='.*' \ 48 | -checks='-fuchsia-*,-cppcoreguidelines-avoid-c-arrays,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-goto,-modernize-avoid-c-arrays,-readability-magic-numbers,-readability-simplify-boolean-expr,-cppcoreguidelines-macro-usage' \ 49 | 50 | # The fix: Add a default target 51 | .PHONY: all 52 | all: 53 | @echo "Makefile processed successfully." -------------------------------------------------------------------------------- /tests/fixtures/advanced_targets/expected.mk: -------------------------------------------------------------------------------- 1 | # Advanced target patterns and dependencies 2 | .PHONY: all clean test install uninstall 3 | 4 | # Double-colon rules with inconsistent formatting 5 | src/%.o:: src/%.c 6 | $(CC) $(CFLAGS) -c $< -o $@ 7 | 8 | src/%.o::src/%.cpp 9 | $(CXX) $(CXXFLAGS) -c $< -o $@ 10 | 11 | # Target with multiple dependency groups 12 | main: $(OBJECTS) $(EXTRA_OBJECTS) \ 13 | $(LIBRARIES) 14 | $(CC) -o $@ $^ $(LDFLAGS) 15 | 16 | # Targets with order-only prerequisites 17 | $(OBJECTS): | $(BUILD_DIR) 18 | 19 | $(BUILD_DIR): 20 | mkdir -p $@ 21 | 22 | # Pattern rules with multiple targets 23 | %.h %.c: %.y 24 | yacc -d $< 25 | mv y.tab.c $*.c 26 | mv y.tab.h $*.h 27 | 28 | # Targets with complex prerequisites 29 | install: all | $(DESTDIR)$(bindir) $(DESTDIR)$(mandir) 30 | install -m755 $(TARGET) $(DESTDIR)$(bindir)/ 31 | install -m644 $(TARGET).1 $(DESTDIR)$(mandir)/ 32 | 33 | # Static pattern rules 34 | $(OBJECTS): %.o: %.c | $(DEPDIR) 35 | $(CC) $(CFLAGS) -c -o $@ $< 36 | $(CC) $(CFLAGS) -MM -MT $@ $< > $(DEPDIR)/$*.d 37 | 38 | # Target-specific variables with poor formatting 39 | debug: CFLAGS += -g -DDEBUG 40 | debug: LDFLAGS += -rdynamic 41 | debug: all 42 | 43 | # Target-specific variable assignment forms that should not warn 44 | ollama/run/cpu: DOCKER_IMAGE_NAME = ollama-wrapper 45 | ollama/run/cpu: DOCKER_UID ?= $(shell id -u) 46 | ollama/run/cpu: DOCKER_GID ?= $(shell id -g) 47 | ollama/run/cpu: MODEL ?= llama3.1 48 | ollama/run/cpu: .make/ollama-docker FORCE 49 | ollama/run/cpu: 50 | docker container rm ollama ; true 51 | docker run \ 52 | --detach \ 53 | --name ollama \ 54 | --publish 11434:11434 \ 55 | --user $(DOCKER_UID):$(DOCKER_GID) \ 56 | --volume $(CURDIR)/data/ollama:/home/ollama/.ollama \ 57 | $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(MODEL) 58 | 59 | release: CFLAGS += -O2 -DNDEBUG 60 | release: CFLAGS += -march=native 61 | release: all 62 | 63 | # Conditional targets 64 | ifeq ($(BUILD_TESTS),yes) 65 | test: $(TEST_OBJECTS) 66 | $(CC) -o test_runner $(TEST_OBJECTS) $(LDFLAGS) 67 | ./test_runner 68 | endif 69 | -------------------------------------------------------------------------------- /tests/fixtures/advanced_targets/input.mk: -------------------------------------------------------------------------------- 1 | # Advanced target patterns and dependencies 2 | .PHONY: all clean test install uninstall 3 | 4 | # Double-colon rules with inconsistent formatting 5 | src/%.o:: src/%.c 6 | $(CC) $(CFLAGS) -c $< -o $@ 7 | 8 | src/%.o::src/%.cpp 9 | $(CXX) $(CXXFLAGS) -c $< -o $@ 10 | 11 | # Target with multiple dependency groups 12 | main: $(OBJECTS) $(EXTRA_OBJECTS) \ 13 | $(LIBRARIES) 14 | $(CC) -o $@ $^ $(LDFLAGS) 15 | 16 | # Targets with order-only prerequisites 17 | $(OBJECTS): | $(BUILD_DIR) 18 | 19 | $(BUILD_DIR): 20 | mkdir -p $@ 21 | 22 | # Pattern rules with multiple targets 23 | %.h %.c: %.y 24 | yacc -d $< 25 | mv y.tab.c $*.c 26 | mv y.tab.h $*.h 27 | 28 | # Targets with complex prerequisites 29 | install: all | $(DESTDIR)$(bindir) $(DESTDIR)$(mandir) 30 | install -m755 $(TARGET) $(DESTDIR)$(bindir)/ 31 | install -m644 $(TARGET).1 $(DESTDIR)$(mandir)/ 32 | 33 | # Static pattern rules 34 | $(OBJECTS): %.o: %.c | $(DEPDIR) 35 | $(CC) $(CFLAGS) -c -o $@ $< 36 | $(CC) $(CFLAGS) -MM -MT $@ $< > $(DEPDIR)/$*.d 37 | 38 | # Target-specific variables with poor formatting 39 | debug: CFLAGS += -g -DDEBUG 40 | debug:LDFLAGS += -rdynamic 41 | debug: all 42 | 43 | # Target-specific variable assignment forms that should not warn 44 | ollama/run/cpu: DOCKER_IMAGE_NAME = ollama-wrapper 45 | ollama/run/cpu: DOCKER_UID ?= $(shell id -u) 46 | ollama/run/cpu: DOCKER_GID ?= $(shell id -g) 47 | ollama/run/cpu: MODEL ?= llama3.1 48 | ollama/run/cpu: .make/ollama-docker FORCE 49 | ollama/run/cpu: 50 | docker container rm ollama ; true 51 | docker run \ 52 | --detach \ 53 | --name ollama \ 54 | --publish 11434:11434 \ 55 | --user $(DOCKER_UID):$(DOCKER_GID) \ 56 | --volume $(CURDIR)/data/ollama:/home/ollama/.ollama \ 57 | $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(MODEL) 58 | 59 | release: CFLAGS += -O2 -DNDEBUG 60 | release:CFLAGS += -march=native 61 | release: all 62 | 63 | # Conditional targets 64 | ifeq ($(BUILD_TESTS),yes) 65 | test: $(TEST_OBJECTS) 66 | $(CC) -o test_runner $(TEST_OBJECTS) $(LDFLAGS) 67 | ./test_runner 68 | endif -------------------------------------------------------------------------------- /tests/fixtures/multiline_variables/expected.mk: -------------------------------------------------------------------------------- 1 | # Test multiline variable assignments with complex scenarios 2 | SOURCES = \ 3 | src/main.c \ 4 | src/utils.c \ 5 | src/parser.c \ 6 | src/lexer.c 7 | 8 | # Variables with mixed line continuations 9 | CFLAGS = -Wall -Wextra \ 10 | -Werror \ 11 | -pedantic 12 | 13 | # Complex variable with embedded quotes and spaces 14 | DEFINES = -DVERSION=\"$(VERSION)\" \ 15 | -DBUILD_DATE="$(shell date)" \ 16 | -DDEBUG=1 \ 17 | -DPLATFORM=\"$(PLATFORM)\" 18 | 19 | # Variable with function calls and complex syntax 20 | OBJECTS = $(patsubst %.c,%.o,$(SOURCES)) \ 21 | $(patsubst %.cpp,%.o,$(wildcard *.cpp)) \ 22 | $(shell find . -name "*.s" | sed 's/\.s/\.o/g') 23 | 24 | # Multiline variable with mixed operators 25 | INSTALL_DIRS += /usr/local/bin \ 26 | /usr/local/share/man/man1 \ 27 | /usr/local/share/doc/$(PACKAGE) 28 | 29 | # Complex substitution with line continuation 30 | CLEANED_SOURCES = $(subst src/,,$(SOURCES:.c=.o)) \ 31 | $(subst tests/,,$(TEST_SOURCES:.c=.o)) \ 32 | $(subst examples/,,$(EXAMPLE_SOURCES:.c=.o)) 33 | 34 | # Variable with conditional assignment and continuation 35 | EXTRA_LIBS ?= -lm \ 36 | -lpthread \ 37 | -ldl 38 | 39 | # Test: Assignment spacing with multi-line values (from demo.mk) 40 | CPPCHECK_FLAGS = --enable=all --inline-suppr \ 41 | --suppress=cstyleCast --suppress=useInitializationList \ 42 | --suppress=nullPointer --suppress=nullPointerRedundantCheck --suppress=ctunullpointer \ 43 | --suppress=unusedFunction --suppress=unusedScopedObject \ 44 | --suppress=useStlAlgorithm \ 45 | 46 | CLANGTIDY_FLAGS = -config='' \ 47 | -header-filter='.*' \ 48 | -checks='-fuchsia-*,-cppcoreguidelines-avoid-c-arrays,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-goto,-modernize-avoid-c-arrays,-readability-magic-numbers,-readability-simplify-boolean-expr,-cppcoreguidelines-macro-usage' \ 49 | 50 | # The fix: Add a default target 51 | .PHONY: all 52 | all: 53 | @echo "Makefile processed successfully." 54 | -------------------------------------------------------------------------------- /mbake/constants/makefile_targets.py: -------------------------------------------------------------------------------- 1 | """Makefile special targets and directives, grouped by semantics.""" 2 | 3 | # Targets that can be duplicated (declarative) 4 | DECLARATIVE_TARGETS = { 5 | ".PHONY", 6 | ".SUFFIXES", 7 | } 8 | 9 | # Targets that affect rule behavior (can appear multiple times) 10 | RULE_BEHAVIOR_TARGETS = { 11 | ".PRECIOUS", 12 | ".INTERMEDIATE", 13 | ".SECONDARY", 14 | ".DELETE_ON_ERROR", 15 | ".IGNORE", 16 | ".SILENT", 17 | } 18 | 19 | # Global directives (should NOT be duplicated) 20 | GLOBAL_DIRECTIVES = { 21 | ".EXPORT_ALL_VARIABLES", 22 | ".NOTPARALLEL", 23 | ".ONESHELL", 24 | ".POSIX", 25 | ".LOW_RESOLUTION_TIME", 26 | ".SECOND_EXPANSION", 27 | ".SECONDEXPANSION", 28 | } 29 | 30 | # Utility/meta targets 31 | UTILITY_TARGETS = { 32 | ".VARIABLES", 33 | ".MAKE", 34 | ".WAIT", 35 | ".INCLUDE_DIRS", 36 | ".LIBPATTERNS", 37 | } 38 | 39 | # All special targets (for easy checking) 40 | ALL_SPECIAL_MAKE_TARGETS = ( 41 | DECLARATIVE_TARGETS | RULE_BEHAVIOR_TARGETS | GLOBAL_DIRECTIVES | UTILITY_TARGETS 42 | ) 43 | 44 | # Default suffixes for GNU Make 45 | DEFAULT_SUFFIXES = { 46 | ".out", 47 | ".a", 48 | ".ln", 49 | ".o", 50 | ".c", 51 | ".cc", 52 | ".C", 53 | ".cpp", 54 | ".p", 55 | ".f", 56 | ".F", 57 | ".m", 58 | ".r", 59 | ".y", 60 | ".l", 61 | ".ym", 62 | ".lm", 63 | ".s", 64 | ".S", 65 | ".mod", 66 | ".sym", 67 | ".def", 68 | ".h", 69 | ".info", 70 | ".dvi", 71 | ".tex", 72 | ".texinfo", 73 | ".texi", 74 | ".txinfo", 75 | ".w", 76 | ".ch", 77 | ".web", 78 | ".sh", 79 | ".elc", 80 | ".el", 81 | } 82 | 83 | # Rule type information 84 | RULE_TYPE_INFO = { 85 | "explicit": {"description": "Direct target:prerequisite definitions"}, 86 | "pattern": {"description": "Pattern-based rules (%.o: %.c)"}, 87 | "suffix": {"description": "Old-style implicit rules (.c.o:)"}, 88 | "static_pattern": {"description": "Rules with specific target lists"}, 89 | "double_colon": {"description": "Rules with :: separator"}, 90 | "special_target": {"description": "Special built-in targets"}, 91 | } 92 | -------------------------------------------------------------------------------- /mbake/plugins/base.py: -------------------------------------------------------------------------------- 1 | """Base plugin interface for bake formatting rules.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import Any 6 | 7 | 8 | @dataclass 9 | class FormatResult: 10 | """Result of a formatting rule application.""" 11 | 12 | lines: list[str] 13 | changed: bool 14 | errors: list[str] 15 | warnings: list[str] 16 | check_messages: list[str] # Messages describing what would change in check mode 17 | 18 | 19 | class FormatterPlugin(ABC): 20 | """Base class for all formatting plugins.""" 21 | 22 | def __init__(self, name: str, priority: int = 50): 23 | """Initialize the plugin. 24 | 25 | Args: 26 | name: Human-readable name of the plugin 27 | priority: Execution priority (lower numbers run first) 28 | """ 29 | self.name = name 30 | self.priority = priority 31 | 32 | @abstractmethod 33 | def format( 34 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 35 | ) -> FormatResult: 36 | """Apply formatting rule to the lines. 37 | 38 | Args: 39 | lines: List of lines to format 40 | config: Configuration dictionary 41 | check_mode: If True, generate descriptive messages about changes 42 | **context: Optional context information (e.g., original_content_ends_with_newline) 43 | 44 | Returns: 45 | FormatResult with updated lines and metadata 46 | """ 47 | pass 48 | 49 | def validate(self, lines: list[str], config: dict) -> list[str]: 50 | """Validate lines according to this rule. 51 | 52 | Args: 53 | lines: List of lines to validate 54 | config: Configuration dictionary 55 | 56 | Returns: 57 | List of validation error messages 58 | """ 59 | result = self.format(lines, config, check_mode=False) 60 | if result.changed: 61 | return [f"{self.name}: formatting violations detected"] 62 | return [] 63 | 64 | def __lt__(self, other: "FormatterPlugin") -> bool: 65 | """Enable sorting by priority.""" 66 | return self.priority < other.priority 67 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | python-version: ['3.9', '3.10', '3.11', '3.12'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -e ".[dev]" 29 | 30 | - name: Run tests 31 | run: | 32 | pytest --cov=mbake --cov-report=xml --cov-report=term 33 | 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v3 36 | if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' 37 | with: 38 | file: ./coverage.xml 39 | fail_ci_if_error: false 40 | continue-on-error: true 41 | 42 | lint: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Set up Python 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: '3.11' 52 | 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | pip install -e ".[dev]" 57 | 58 | - name: Run black 59 | run: black --check mbake tests 60 | 61 | - name: Run ruff 62 | run: ruff check mbake tests 63 | 64 | - name: Run mypy 65 | run: mypy mbake 66 | 67 | package: 68 | runs-on: ubuntu-latest 69 | 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: Set up Python 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: '3.11' 77 | 78 | - name: Install dependencies 79 | run: | 80 | python -m pip install --upgrade pip 81 | pip install build twine 82 | 83 | - name: Build package 84 | run: python -m build 85 | 86 | - name: Check package 87 | run: twine check dist/* -------------------------------------------------------------------------------- /mbake/core/rules/whitespace.py: -------------------------------------------------------------------------------- 1 | """Whitespace cleanup rule for Makefiles.""" 2 | 3 | from typing import Any 4 | 5 | from ...plugins.base import FormatResult, FormatterPlugin 6 | 7 | 8 | class WhitespaceRule(FormatterPlugin): 9 | """Handles trailing whitespace removal and line normalization.""" 10 | 11 | def __init__(self) -> None: 12 | super().__init__( 13 | "whitespace", priority=45 14 | ) # Run late to clean up after other rules 15 | 16 | def format( 17 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 18 | ) -> FormatResult: 19 | """Remove trailing whitespace and normalize empty lines.""" 20 | formatted_lines = [] 21 | changed = False 22 | errors: list[str] = [] 23 | warnings: list[str] = [] 24 | 25 | remove_trailing_whitespace = config.get("remove_trailing_whitespace", True) 26 | normalize_empty_lines = config.get("normalize_empty_lines", True) 27 | 28 | prev_was_empty = False 29 | 30 | for line in lines: 31 | # Remove trailing whitespace if enabled 32 | if remove_trailing_whitespace: 33 | cleaned_line = line.rstrip() 34 | if cleaned_line != line: 35 | changed = True 36 | line = cleaned_line 37 | 38 | # Normalize consecutive empty lines if enabled 39 | if normalize_empty_lines: 40 | is_empty = not line.strip() 41 | if is_empty and prev_was_empty: 42 | # Skip this empty line (already have one) 43 | changed = True 44 | continue 45 | prev_was_empty = is_empty 46 | 47 | formatted_lines.append(line) 48 | 49 | # Remove extra trailing empty lines 50 | while ( 51 | len(formatted_lines) > 1 52 | and formatted_lines[-1] == "" 53 | and formatted_lines[-2] == "" 54 | ): 55 | formatted_lines.pop() 56 | changed = True 57 | 58 | return FormatResult( 59 | lines=formatted_lines, 60 | changed=changed, 61 | errors=errors, 62 | warnings=warnings, 63 | check_messages=[], 64 | ) 65 | -------------------------------------------------------------------------------- /mbake/core/rules/rule_type_detection.py: -------------------------------------------------------------------------------- 1 | """Rule type detection and classification for Makefiles.""" 2 | 3 | import re 4 | from enum import Enum 5 | from typing import Any 6 | 7 | from ...plugins.base import FormatResult, FormatterPlugin 8 | 9 | 10 | class RuleType(Enum): 11 | EXPLICIT = "explicit" 12 | PATTERN = "pattern" 13 | SUFFIX = "suffix" 14 | STATIC_PATTERN = "static_pattern" 15 | DOUBLE_COLON = "double_colon" 16 | SPECIAL_TARGET = "special_target" 17 | 18 | 19 | class RuleTypeDetectionRule(FormatterPlugin): 20 | """Detects and classifies different types of Makefile rules.""" 21 | 22 | def __init__(self) -> None: 23 | super().__init__("rule_type_detection", priority=5) 24 | 25 | def format( 26 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 27 | ) -> FormatResult: 28 | """Detect and classify rule types.""" 29 | rule_types = {} 30 | 31 | for i, line in enumerate(lines): 32 | stripped = line.strip() 33 | if ":" in stripped and not line.startswith("\t"): 34 | rule_type = self._classify_rule(stripped, lines) 35 | rule_types[i] = rule_type 36 | 37 | # Store rule types in context for other rules to use 38 | context["rule_types"] = rule_types 39 | 40 | return FormatResult( 41 | lines=lines, changed=False, errors=[], warnings=[], check_messages=[] 42 | ) 43 | 44 | def _classify_rule(self, line: str, all_lines: list[str]) -> RuleType: 45 | """Classify the type of rule.""" 46 | # Special targets 47 | if line.startswith(".") and ":" in line: 48 | return RuleType.SPECIAL_TARGET 49 | 50 | # Double-colon rules 51 | if "::" in line: 52 | return RuleType.DOUBLE_COLON 53 | 54 | # Static pattern rules (targets: pattern: prerequisites) 55 | if re.search(r".*:\s*%.*\s*:\s*", line): 56 | return RuleType.STATIC_PATTERN 57 | 58 | # Pattern rules (%.o: %.c) 59 | if "%" in line and ":" in line: 60 | return RuleType.PATTERN 61 | 62 | # Suffix rules (.a.b:) 63 | if re.match(r"^\.[^:]+\.\w+:", line): 64 | return RuleType.SUFFIX 65 | 66 | # Default to explicit rule 67 | return RuleType.EXPLICIT 68 | -------------------------------------------------------------------------------- /mbake/core/rules/final_newline.py: -------------------------------------------------------------------------------- 1 | """Final newline rule for Makefiles.""" 2 | 3 | from typing import Any 4 | 5 | from ...plugins.base import FormatResult, FormatterPlugin 6 | 7 | 8 | class FinalNewlineRule(FormatterPlugin): 9 | """Ensures files end with a final newline if configured.""" 10 | 11 | def __init__(self) -> None: 12 | super().__init__( 13 | "final_newline", priority=70 14 | ) # Run late, after content changes 15 | 16 | def format( 17 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 18 | ) -> FormatResult: 19 | """Ensure final newline if configured.""" 20 | ensure_final_newline = config.get("ensure_final_newline", True) 21 | 22 | if not ensure_final_newline: 23 | return FormatResult( 24 | lines=lines, changed=False, errors=[], warnings=[], check_messages=[] 25 | ) 26 | 27 | # Check if file is empty 28 | if not lines: 29 | return FormatResult( 30 | lines=lines, changed=False, errors=[], warnings=[], check_messages=[] 31 | ) 32 | 33 | formatted_lines = list(lines) 34 | changed = False 35 | errors: list[str] = [] 36 | warnings: list[str] = [] 37 | check_messages: list[str] = [] 38 | 39 | # Check if the last line ends with a newline 40 | # In check mode, respect the original_content_ends_with_newline parameter 41 | original_ends_with_newline = context.get( 42 | "original_content_ends_with_newline", False 43 | ) 44 | 45 | # If original content already ends with newline, no change needed 46 | if check_mode and original_ends_with_newline: 47 | return FormatResult( 48 | lines=lines, changed=False, errors=[], warnings=[], check_messages=[] 49 | ) 50 | 51 | # If the last line is not empty, we need to add a newline 52 | if formatted_lines and formatted_lines[-1] != "": 53 | if check_mode: 54 | # Generate check message 55 | line_count = len(formatted_lines) 56 | gnu_format = config.get("_global", {}).get("gnu_error_format", True) 57 | 58 | if gnu_format: 59 | message = f"{line_count}: Warning: Missing final newline" 60 | else: 61 | message = f"Warning: Missing final newline (line {line_count})" 62 | 63 | check_messages.append(message) 64 | else: 65 | # Add empty line to ensure final newline 66 | formatted_lines.append("") 67 | 68 | changed = True 69 | 70 | return FormatResult( 71 | lines=formatted_lines, 72 | changed=changed, 73 | errors=errors, 74 | warnings=warnings, 75 | check_messages=check_messages, 76 | ) 77 | -------------------------------------------------------------------------------- /completions/zsh/_mbake: -------------------------------------------------------------------------------- 1 | #compdef mbake 2 | 3 | _mbake() { 4 | local curcontext="$curcontext" state line 5 | typeset -A opt_args 6 | 7 | _arguments -C \ 8 | '1: :->cmds' \ 9 | '*:: :->args' 10 | 11 | case $state in 12 | cmds) 13 | _values 'mbake commands' \ 14 | 'init[Initialize configuration file]' \ 15 | 'config[Show current configuration]' \ 16 | 'validate[Validate Makefile syntax]' \ 17 | 'format[Format Makefiles]' \ 18 | 'update[Update mbake]' \ 19 | 'completions[Generate shell completions]' 20 | ;; 21 | args) 22 | case $line[1] in 23 | init) 24 | _arguments \ 25 | '--force[Overwrite existing config]' \ 26 | '--config[Path to configuration file]' \ 27 | '--help[Show help]' 28 | ;; 29 | config) 30 | _arguments \ 31 | '--path[Show config file path]' \ 32 | '--config[Path to configuration file]' \ 33 | '--help[Show help]' 34 | ;; 35 | validate) 36 | _arguments \ 37 | '--config[Path to configuration file]' \ 38 | '--verbose[Enable verbose output]' \ 39 | '-v[Enable verbose output]' \ 40 | '--help[Show help]' 41 | ;; 42 | format) 43 | _arguments \ 44 | '--check[Check formatting without changes]' \ 45 | '-c[Check formatting without changes]' \ 46 | '--diff[Show diff of changes]' \ 47 | '-d[Show diff of changes]' \ 48 | '--verbose[Enable verbose output]' \ 49 | '-v[Enable verbose output]' \ 50 | '--debug[Enable debug output]' \ 51 | '--config[Path to configuration file]' \ 52 | '--backup[Create backup files]' \ 53 | '-b[Create backup files]' \ 54 | '--validate[Validate syntax after formatting]' \ 55 | '--help[Show help]' 56 | ;; 57 | update) 58 | _arguments \ 59 | '--force[Force update]' \ 60 | '--check[Only check for updates]' \ 61 | '--yes[Skip confirmation]' \ 62 | '-y[Skip confirmation]' \ 63 | '--help[Show help]' 64 | ;; 65 | completions) 66 | _values 'shell types' 'bash' 'zsh' 'fish' 67 | ;; 68 | esac 69 | ;; 70 | esac 71 | } 72 | 73 | _mbake "$@" -------------------------------------------------------------------------------- /tests/fixtures/edge_cases_and_quirks/expected.mk: -------------------------------------------------------------------------------- 1 | #Edge cases and Makefile quirks 2 | 3 | #Variables with special characters 4 | WEIRD_VAR = value with spaces 5 | PATH_VAR = /path/with/$(DOLLAR)sign 6 | QUOTE_VAR = "quoted value" 7 | ESCAPE_VAR = value\ with\ escapes 8 | 9 | #Variables with complex substitutions 10 | NESTED = $(subst $(SPACE),_,$(strip $(SOURCES))) 11 | SPACE := 12 | SPACE += 13 | 14 | #Dollar sign handling 15 | DOLLAR = $$ 16 | DOUBLE_DOLLAR = $$(echo hello) 17 | LITERAL_DOLLAR = $$$$ 18 | 19 | #Targets with special characters 20 | target-with-dashes: dependency 21 | @echo "Building target with dashes" 22 | 23 | target_with_underscores: dependency 24 | @echo "Building target with underscores" 25 | 26 | #Targets with variables in names 27 | $(TARGET).backup: $(TARGET) 28 | cp $< $@ 29 | 30 | #Pattern rules with edge cases 31 | %.out: %.in 32 | cp $< $@ 33 | 34 | src/%.o: src/%.c 35 | $(CC) -c $< -o $@ 36 | 37 | #Multiple targets on one line 38 | target1 target2 target3: common-dep 39 | @echo "Multiple targets" 40 | 41 | #Targets with no dependencies 42 | standalone: 43 | @echo "Standalone target" 44 | 45 | #Empty recipe 46 | empty-recipe: 47 | 48 | #Recipe with only whitespace 49 | whitespace-only: 50 | 51 | #Long lines that might need wrapping 52 | very-long-target-name-that-might-cause-formatting-issues: very-long-dependency-name-that-also-might-cause-issues 53 | very-long-command-line-that-extends-beyond-normal-width-and-might-need-special-handling-by-the-formatter 54 | 55 | #Conditional assignments with complex conditions 56 | ifeq ($(origin CC),undefined) 57 | CC = gcc 58 | endif 59 | 60 | ifneq (,$(findstring gcc,$(CC))) 61 | COMPILER_FLAGS = -Wall -Wextra 62 | endif 63 | 64 | #Complex shell constructs in recipes 65 | complex-shell: 66 | for i in 1 2 3; do \ 67 | echo "Processing $$i"; \ 68 | if [ $$i -eq 2 ]; then \ 69 | continue; \ 70 | fi; \ 71 | echo "Done with $$i"; \ 72 | done 73 | 74 | #Variable assignments with functions 75 | FILES := $(wildcard *.c) 76 | OBJS=$(FILES:.c=.o) 77 | DEPS=$(OBJS:.o=.d) 78 | 79 | #Immediate vs deferred evaluation edge cases 80 | NOW := $(shell date) 81 | LATER = $(shell date) 82 | 83 | #Include with variables and functions 84 | -include $(DEPS) 85 | include $(wildcard config/*.mk) 86 | 87 | #Comments in weird places 88 | CC = gcc#inline comment 89 | #CFLAGS=-Wall#commented out assignment 90 | 91 | #Tab vs spaces in recipes (this tests tab handling) 92 | tab-test: 93 | echo "This line uses tab" 94 | echo "This line uses spaces (should be converted to tab)" 95 | echo "This line uses mixed tab and spaces" 96 | 97 | #Function calls with complex arguments 98 | FILTERED = $(filter-out $(EXCLUDE_PATTERNS),$(ALL_FILES)) 99 | TRANSFORMED = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES)) 100 | 101 | #Export with complex expressions 102 | export PATH:=(PATH):$(shell pwd)/bin 103 | export CFLAGS+=(if $(DEBUG),-g -O0,-O2) 104 | 105 | .PHONY: dependency 106 | dependency: 107 | @echo "Satisfying the 'dependency' requirement." 108 | -------------------------------------------------------------------------------- /tests/fixtures/edge_cases_and_quirks/input.mk: -------------------------------------------------------------------------------- 1 | #Edge cases and Makefile quirks 2 | 3 | #Variables with special characters 4 | WEIRD_VAR=value with spaces 5 | PATH_VAR=/path/with/$(DOLLAR)sign 6 | QUOTE_VAR="quoted value" 7 | ESCAPE_VAR=value\ with\ escapes 8 | 9 | #Variables with complex substitutions 10 | NESTED=$(subst $(SPACE),_,$(strip $(SOURCES))) 11 | SPACE:= 12 | SPACE+= 13 | 14 | #Dollar sign handling 15 | DOLLAR=$$ 16 | DOUBLE_DOLLAR=$$(echo hello) 17 | LITERAL_DOLLAR=$$$$ 18 | 19 | #Targets with special characters 20 | target-with-dashes:dependency 21 | @echo "Building target with dashes" 22 | 23 | target_with_underscores:dependency 24 | @echo "Building target with underscores" 25 | 26 | #Targets with variables in names 27 | $(TARGET).backup:$(TARGET) 28 | cp $< $@ 29 | 30 | #Pattern rules with edge cases 31 | %.out:%.in 32 | cp $< $@ 33 | 34 | src/%.o:src/%.c 35 | $(CC) -c $< -o $@ 36 | 37 | #Multiple targets on one line 38 | target1 target2 target3:common-dep 39 | @echo "Multiple targets" 40 | 41 | #Targets with no dependencies 42 | standalone: 43 | @echo "Standalone target" 44 | 45 | #Empty recipe 46 | empty-recipe: 47 | 48 | #Recipe with only whitespace 49 | whitespace-only: 50 | 51 | 52 | #Long lines that might need wrapping 53 | very-long-target-name-that-might-cause-formatting-issues:very-long-dependency-name-that-also-might-cause-issues 54 | very-long-command-line-that-extends-beyond-normal-width-and-might-need-special-handling-by-the-formatter 55 | 56 | #Conditional assignments with complex conditions 57 | ifeq ($(origin CC),undefined) 58 | CC=gcc 59 | endif 60 | 61 | ifneq (,$(findstring gcc,$(CC))) 62 | COMPILER_FLAGS=-Wall -Wextra 63 | endif 64 | 65 | #Complex shell constructs in recipes 66 | complex-shell: 67 | for i in 1 2 3; do \ 68 | echo "Processing $$i"; \ 69 | if [ $$i -eq 2 ]; then \ 70 | continue; \ 71 | fi; \ 72 | echo "Done with $$i"; \ 73 | done 74 | 75 | #Variable assignments with functions 76 | FILES:=$(wildcard *.c) 77 | OBJS=$(FILES:.c=.o) 78 | DEPS=$(OBJS:.o=.d) 79 | 80 | #Immediate vs deferred evaluation edge cases 81 | NOW:=$(shell date) 82 | LATER=$(shell date) 83 | 84 | #Include with variables and functions 85 | -include $(DEPS) 86 | include $(wildcard config/*.mk) 87 | 88 | #Comments in weird places 89 | CC=gcc#inline comment 90 | #CFLAGS=-Wall#commented out assignment 91 | 92 | #Tab vs spaces in recipes (this tests tab handling) 93 | tab-test: 94 | echo "This line uses tab" 95 | echo "This line uses spaces (should be converted to tab)" 96 | echo "This line uses mixed tab and spaces" 97 | 98 | #Function calls with complex arguments 99 | FILTERED=$(filter-out $(EXCLUDE_PATTERNS),$(ALL_FILES)) 100 | TRANSFORMED=$(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES)) 101 | 102 | #Export with complex expressions 103 | export PATH:=(PATH):$(shell pwd)/bin 104 | export CFLAGS+=(if $(DEBUG),-g -O0,-O2) 105 | 106 | .PHONY: dependency 107 | dependency: 108 | @echo "Satisfying the 'dependency' requirement." -------------------------------------------------------------------------------- /completions/fish/mbake.fish: -------------------------------------------------------------------------------- 1 | # fish completion for mbake 2 | 3 | complete -c mbake -n "__fish_use_subcommand" -s h -l help -d "Show this help message and exit" 4 | complete -c mbake -n "__fish_use_subcommand" -l version -d "Show version and exit" 5 | 6 | complete -c mbake -n "__fish_use_subcommand" -a init -d "Initialize configuration file with defaults" 7 | complete -c mbake -n "__fish_use_subcommand" -a config -d "Show current configuration" 8 | complete -c mbake -n "__fish_use_subcommand" -a validate -d "Validate Makefile syntax" 9 | complete -c mbake -n "__fish_use_subcommand" -a format -d "Format Makefiles" 10 | complete -c mbake -n "__fish_use_subcommand" -a update -d "Update mbake to the latest version from PyPI" 11 | complete -c mbake -n "__fish_use_subcommand" -a completions -d "Generate shell completion scripts" 12 | 13 | # init command 14 | complete -c mbake -n "__fish_seen_subcommand_from init" -l force -d "Overwrite existing config" 15 | complete -c mbake -n "__fish_seen_subcommand_from init" -l config -r -d "Path to configuration file" 16 | complete -c mbake -n "__fish_seen_subcommand_from init" -s h -l help -d "Show this help message and exit" 17 | 18 | # config command 19 | complete -c mbake -n "__fish_seen_subcommand_from config" -l path -d "Show config file path" 20 | complete -c mbake -n "__fish_seen_subcommand_from config" -l config -r -d "Path to configuration file" 21 | complete -c mbake -n "__fish_seen_subcommand_from config" -s h -l help -d "Show this help message and exit" 22 | 23 | # validate command 24 | complete -c mbake -n "__fish_seen_subcommand_from validate" -l config -r -d "Path to configuration file" 25 | complete -c mbake -n "__fish_seen_subcommand_from validate" -l verbose -s v -d "Enable verbose output" 26 | complete -c mbake -n "__fish_seen_subcommand_from validate" -s h -l help -d "Show this help message and exit" 27 | 28 | # format command 29 | complete -c mbake -n "__fish_seen_subcommand_from format" -l check -s c -d "Check formatting without changes" 30 | complete -c mbake -n "__fish_seen_subcommand_from format" -l diff -s d -d "Show diff of changes" 31 | complete -c mbake -n "__fish_seen_subcommand_from format" -l verbose -s v -d "Enable verbose output" 32 | complete -c mbake -n "__fish_seen_subcommand_from format" -l debug -d "Enable debug output" 33 | complete -c mbake -n "__fish_seen_subcommand_from format" -l config -r -d "Path to configuration file" 34 | complete -c mbake -n "__fish_seen_subcommand_from format" -l backup -s b -d "Create backup files" 35 | complete -c mbake -n "__fish_seen_subcommand_from format" -l validate -d "Validate syntax after formatting" 36 | complete -c mbake -n "__fish_seen_subcommand_from format" -s h -l help -d "Show this help message and exit" 37 | 38 | # update command 39 | complete -c mbake -n "__fish_seen_subcommand_from update" -l force -d "Force update even if up to date" 40 | complete -c mbake -n "__fish_seen_subcommand_from update" -l check -d "Only check, don't update" 41 | complete -c mbake -n "__fish_seen_subcommand_from update" -l yes -s y -d "Skip confirmation prompt" 42 | complete -c mbake -n "__fish_seen_subcommand_from update" -s h -l help -d "Show this help message and exit" 43 | 44 | # completions command 45 | complete -c mbake -n "__fish_seen_subcommand_from completions" -a "bash zsh fish" -d "Shell type" 46 | complete -c mbake -n "__fish_seen_subcommand_from completions" -s h -l help -d "Show this help message and exit" -------------------------------------------------------------------------------- /mbake/core/rules/target_validation.py: -------------------------------------------------------------------------------- 1 | """Rule for validating target syntax and warning about invalid constructs.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | from ...utils.line_utils import LineUtils 8 | from ...utils.pattern_utils import PatternUtils 9 | 10 | 11 | class TargetValidationRule(FormatterPlugin): 12 | """Validates target syntax and warns about invalid constructs.""" 13 | 14 | def __init__(self) -> None: 15 | super().__init__( 16 | "target_validation", priority=6 17 | ) # Run after duplicate detection 18 | 19 | def format( 20 | self, 21 | lines: list[str], 22 | config: dict[str, Any], 23 | check_mode: bool = False, 24 | **context: Any, 25 | ) -> FormatResult: 26 | """Validate target syntax and return warnings.""" 27 | warnings = self._validate_target_syntax(lines, config) 28 | # This rule doesn't modify content, just reports warnings 29 | return FormatResult( 30 | lines=lines, changed=False, errors=[], warnings=warnings, check_messages=[] 31 | ) 32 | 33 | def _validate_target_syntax( 34 | self, lines: list[str], config: dict[str, Any] 35 | ) -> list[str]: 36 | """Check for invalid target syntax patterns.""" 37 | warnings = [] 38 | 39 | for i, line in enumerate(lines, 1): 40 | stripped = line.strip() 41 | 42 | # Skip empty lines and comments 43 | if not stripped or stripped.startswith("#"): 44 | continue 45 | 46 | # Get active recipe prefix for this line 47 | active_prefix = LineUtils.get_active_recipe_prefix(lines, i - 1) 48 | 49 | # Skip recipe lines and their continuations entirely (shell context) 50 | if line.startswith("\t") or LineUtils.is_recipe_line(line, i - 1, lines): 51 | continue 52 | 53 | # Check for invalid target syntax 54 | if self._is_invalid_target(line, active_prefix): 55 | line_num = i + 1 56 | gnu_format = config.get("_global", {}).get("gnu_error_format", True) 57 | if gnu_format: 58 | warnings.append( 59 | f"{line_num}: Warning: Invalid target syntax: {stripped}" 60 | ) 61 | else: 62 | warnings.append( 63 | f"Warning: Invalid target syntax: {stripped} (line {line_num})" 64 | ) 65 | 66 | return warnings 67 | 68 | def _is_invalid_target(self, line: str, active_prefix: str) -> bool: 69 | """Check if line contains invalid target syntax.""" 70 | stripped = line.strip() 71 | 72 | # Check for target-like 'name=value: ...' only when there is no space after '=' 73 | # and the value before ':' is not colon-safe (URL-like, ISO datetime, path) 74 | if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*=\S*:\S*", stripped): 75 | after_eq = stripped.split("=", 1)[1] 76 | value_before_colon = after_eq.split(":", 1)[0] 77 | if not PatternUtils.value_is_colon_safe(value_before_colon): 78 | return True 79 | 80 | # Check for target preceded by .RECIPEPREFIX character 81 | if LineUtils.is_recipe_line_with_prefix(line, active_prefix): 82 | return False 83 | 84 | return False 85 | -------------------------------------------------------------------------------- /mbake/core/rules/pattern_spacing.py: -------------------------------------------------------------------------------- 1 | """Pattern rule spacing rule for Makefiles.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | 8 | 9 | class PatternSpacingRule(FormatterPlugin): 10 | """Handles spacing in pattern rules and static pattern rules.""" 11 | 12 | def __init__(self) -> None: 13 | super().__init__("pattern_spacing", priority=17) 14 | 15 | def format( 16 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 17 | ) -> FormatResult: 18 | """Normalize spacing in pattern rules.""" 19 | formatted_lines = [] 20 | changed = False 21 | errors: list[str] = [] 22 | warnings: list[str] = [] 23 | 24 | space_after_colon = config.get("space_after_colon", True) 25 | 26 | for line in lines: 27 | # Skip empty lines, comments, and recipe lines 28 | stripped = line.strip() 29 | if not stripped or stripped.startswith("#") or line.startswith("\t"): 30 | formatted_lines.append(line) 31 | continue 32 | 33 | # Process pattern rule spacing 34 | new_line = self._format_pattern_rule(line, space_after_colon) 35 | if new_line != line: 36 | changed = True 37 | formatted_lines.append(new_line) 38 | else: 39 | formatted_lines.append(line) 40 | 41 | return FormatResult( 42 | lines=formatted_lines, 43 | changed=changed, 44 | errors=errors, 45 | warnings=warnings, 46 | check_messages=[], 47 | ) 48 | 49 | def _format_pattern_rule(self, line: str, space_after_colon: bool) -> str: 50 | """Format spacing in pattern rules.""" 51 | # Handle static pattern rules with two colons: targets: pattern: prerequisites 52 | if re.search(r".*:\s*%.*\s*:\s*", line) and not re.search(r"[=]", line): 53 | static_pattern_match = re.match( 54 | r"^(\s*)([^:]+):\s*([^:]+)\s*:\s*(.*)$", line 55 | ) 56 | if static_pattern_match: 57 | leading_whitespace = static_pattern_match.group(1) 58 | targets_part = static_pattern_match.group(2).rstrip() 59 | pattern_part = static_pattern_match.group(3).strip() 60 | prereqs_part = static_pattern_match.group(4).strip() 61 | 62 | new_line = ( 63 | leading_whitespace 64 | + f"{targets_part}: {pattern_part}: {prereqs_part}" 65 | ) 66 | return new_line 67 | 68 | # Handle simple pattern rules: %.o: %.c 69 | elif re.search(r"%.*:", line) and line.count(":") == 1: 70 | pattern_match = re.match(r"^(\s*)([^:]+):(.*)$", line) 71 | if pattern_match: 72 | leading_whitespace = pattern_match.group(1) 73 | pattern_part = pattern_match.group(2).rstrip() 74 | prereqs_part = pattern_match.group(3) 75 | 76 | if space_after_colon: 77 | if prereqs_part.startswith(" "): 78 | prereqs_part = " " + prereqs_part.lstrip() 79 | elif prereqs_part: 80 | prereqs_part = " " + prereqs_part 81 | else: 82 | prereqs_part = prereqs_part.lstrip() 83 | 84 | new_line = leading_whitespace + pattern_part + ":" + prereqs_part 85 | return new_line 86 | 87 | return line 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # IDEs and editors 156 | .vscode/ 157 | .idea/ 158 | *.swp 159 | *.swo 160 | *~ 161 | 162 | # Node.js 163 | node_modules/ 164 | npm-debug.log* 165 | yarn-debug.log* 166 | yarn-error.log* 167 | package-lock.json 168 | 169 | # OS generated files 170 | .DS_Store 171 | .DS_Store? 172 | ._* 173 | .Spotlight-V100 174 | .Trashes 175 | ehthumbs.db 176 | Thumbs.db 177 | 178 | # Temporary files 179 | *.tmp 180 | *.temp 181 | 182 | # Other 183 | PUBLISHING.md 184 | -------------------------------------------------------------------------------- /tests/verilator/Makefile.txt: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # 3 | # DESCRIPTION: Verilator Example: Small Makefile 4 | # 5 | # This calls the object directory makefile. That allows the objects to 6 | # be placed in the "current directory" which simplifies the Makefile. 7 | # 8 | # This file ONLY is placed under the Creative Commons Public Domain, for 9 | # any use, without warranty, 2020 by Wilson Snyder. 10 | # SPDX-License-Identifier: CC0-1.0 11 | # 12 | ###################################################################### 13 | # Check for sanity to avoid later confusion 14 | 15 | ifneq ($(words $(CURDIR)),1) 16 | $(error Unsupported: GNU Make cannot build in directories containing spaces, build elsewhere: '$(CURDIR)') 17 | endif 18 | 19 | ###################################################################### 20 | # Set up variables 21 | 22 | # If $VERILATOR_ROOT isn't in the environment, we assume it is part of a 23 | # package install, and verilator is in your path. Otherwise find the 24 | # binary relative to $VERILATOR_ROOT (such as when inside the git sources). 25 | ifeq ($(VERILATOR_ROOT),) 26 | VERILATOR = verilator 27 | VERILATOR_COVERAGE = verilator_coverage 28 | else 29 | export VERILATOR_ROOT 30 | VERILATOR = $(VERILATOR_ROOT)/bin/verilator 31 | VERILATOR_COVERAGE = $(VERILATOR_ROOT)/bin/verilator_coverage 32 | endif 33 | 34 | VERILATOR_FLAGS = 35 | # Generate SystemC in executable form 36 | VERILATOR_FLAGS += -sc --exe 37 | # Generate makefile dependencies (not shown as complicates the Makefile) 38 | #VERILATOR_FLAGS += -MMD 39 | # Optimize 40 | VERILATOR_FLAGS += -x-assign fast 41 | # Warn abount lint issues; may not want this on less solid designs 42 | VERILATOR_FLAGS += -Wall 43 | # Make waveforms 44 | VERILATOR_FLAGS += --trace-vcd 45 | # Check SystemVerilog assertions 46 | VERILATOR_FLAGS += --assert 47 | # Generate coverage analysis 48 | VERILATOR_FLAGS += --coverage 49 | # Run Verilator in debug mode 50 | #VERILATOR_FLAGS += --debug 51 | # Add this trace to get a backtrace in gdb 52 | #VERILATOR_FLAGS += --gdbbt 53 | 54 | # Input files for Verilator 55 | VERILATOR_INPUT = -f input.vc top.v sc_main.cpp 56 | 57 | # Check if SC exists via a verilator call (empty if not) 58 | SYSTEMC_EXISTS := $(shell $(VERILATOR) --get-supported SYSTEMC) 59 | 60 | ###################################################################### 61 | 62 | ifneq ($(SYSTEMC_EXISTS),) 63 | default: run 64 | else 65 | default: nosc 66 | endif 67 | 68 | run: 69 | @echo 70 | @echo "-- Verilator tracing example" 71 | 72 | @echo 73 | @echo "-- VERILATE ----------------" 74 | $(VERILATOR) $(VERILATOR_FLAGS) $(VERILATOR_INPUT) 75 | 76 | @echo 77 | @echo "-- COMPILE -----------------" 78 | # To compile, we can either 79 | # 1. Pass --build to Verilator by editing VERILATOR_FLAGS above. 80 | # 2. Or, run the make rules Verilator does: 81 | # $(MAKE) -j -C obj_dir -f Vtop.mk 82 | # 3. Or, call a submakefile where we can override the rules ourselves: 83 | $(MAKE) -j -C obj_dir -f ../Makefile_obj 84 | 85 | @echo 86 | @echo "-- RUN ---------------------" 87 | @rm -rf logs 88 | @mkdir -p logs 89 | obj_dir/Vtop +trace 90 | 91 | @echo 92 | @echo "-- COVERAGE ----------------" 93 | @rm -rf logs/annotated 94 | $(VERILATOR_COVERAGE) --annotate logs/annotated logs/coverage.dat 95 | 96 | @echo 97 | @echo "-- DONE --------------------" 98 | @echo "To see waveforms, open vlt_dump.vcd in a waveform viewer" 99 | @echo 100 | 101 | ###################################################################### 102 | # Other targets 103 | 104 | nosc: 105 | @echo 106 | @echo "%Skip: SYSTEMC_INCLUDE not in environment" 107 | @echo "(If you have SystemC see the README, and rebuild Verilator)" 108 | @echo 109 | 110 | show-config: 111 | $(VERILATOR) -V 112 | 113 | maintainer-copy: : 114 | clean mostlyclean distclean maintainer-clean: : 115 | -rm -rf obj_dir logs *.log *.dmp *.vpd coverage.dat core 116 | -------------------------------------------------------------------------------- /tests/verilator/Makefile-2.in: -------------------------------------------------------------------------------- 1 | .PHONY: ../bin/verilator_bin$(EXEEXT) ../bin/verilator_bin_dbg$(EXEEXT) ../bin/verilator_coverage_bin_dbg$(EXEEXT) 2 | 3 | # -*- Makefile -*- 4 | #***************************************************************************** 5 | # 6 | # DESCRIPTION: Verilator: Makefile for verilog source 7 | # 8 | # Code available from: https://verilator.org 9 | # 10 | #***************************************************************************** 11 | # 12 | # Copyright 2003-2025 by Wilson Snyder. This program is free software; you 13 | # can redistribute it and/or modify it under the terms of either the GNU 14 | # Lesser General Public License Version 3 or the Perl Artistic License 15 | # Version 2.0. 16 | # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 17 | # 18 | #****************************************************************************/ 19 | 20 | #### Start of system configuration section. #### 21 | 22 | srcdir = @srcdir@ 23 | EXEEXT = @EXEEXT@ 24 | PYTHON3 = @PYTHON3@ 25 | # VPATH only does sources; fix install_test's not creating ../bin 26 | vpath %.h @srcdir@ 27 | 28 | #### End of system configuration section. #### 29 | 30 | default: dbg opt 31 | debug: dbg 32 | optimize: opt 33 | 34 | ifneq ($(words $(CURDIR)),1) 35 | $(error Unsupported: GNU Make cannot build in directories containing spaces, build elsewhere: '$(CURDIR)') 36 | endif 37 | 38 | UNDER_GIT = $(wildcard ${srcdir}/../.git/logs/HEAD) 39 | 40 | ifeq (,$(wildcard obj_dbg/bear.o)) 41 | ifneq (, $(shell which bear 2>/dev/null)) 42 | BEAR := $(shell which bear) 43 | ifeq (, $(shell $(BEAR) --output obj_dbg/comptest.json -- true)) 44 | $(shell which bear 2>/dev/null >obj_dbg/bear.o) 45 | else 46 | # unsupported version 47 | BEAR := 48 | endif 49 | endif 50 | else 51 | BEAR := $(shell cat obj_dbg/bear.o) 52 | endif 53 | 54 | ifneq ($(BEAR),) 55 | BEAR_OBJ_OPT := $(BEAR) --append --output obj_dbg/compile_commands.json -- 56 | else 57 | BEAR_OBJ_OPT := 58 | endif 59 | 60 | #********************************************************************* 61 | 62 | obj_opt: 63 | mkdir -p $@ 64 | obj_dbg: 65 | mkdir -p $@ 66 | ../bin: 67 | mkdir -p $@ 68 | 69 | .SUFFIXES: 70 | 71 | opt: ../bin/verilator_bin$(EXEEXT) 72 | ifeq ($(VERILATOR_NO_OPT_BUILD),1) # Faster laptop development... One build 73 | ../bin/verilator_bin$(EXEEXT): ../bin/verilator_bin_dbg$(EXEEXT) 74 | -cp -p $< $@.tmp 75 | -mv -f $@.tmp $@ 76 | else 77 | ../bin/verilator_bin$(EXEEXT): obj_opt ../bin prefiles 78 | $(MAKE) -C obj_opt -j 1 TGT=../$@ -f ../Makefile_obj serial 79 | $(MAKE) -C obj_opt TGT=../$@ -f ../Makefile_obj 80 | endif 81 | 82 | dbg: ../bin/verilator_bin_dbg$(EXEEXT) ../bin/verilator_coverage_bin_dbg$(EXEEXT) 83 | ../bin/verilator_bin_dbg$(EXEEXT): obj_dbg ../bin prefiles 84 | $(BEAR_OBJ_OPT) $(MAKE) -C obj_dbg -j 1 TGT=../$@ VL_DEBUG=1 -f ../Makefile_obj serial 85 | $(BEAR_OBJ_OPT) $(MAKE) -C obj_dbg TGT=../$@ VL_DEBUG=1 -f ../Makefile_obj 86 | 87 | ../bin/verilator_coverage_bin_dbg$(EXEEXT): obj_dbg ../bin prefiles 88 | $(MAKE) -C obj_dbg TGT=../$@ VL_DEBUG=1 VL_VLCOV=1 -f ../Makefile_obj serial_vlcov 89 | $(MAKE) -C obj_dbg TGT=../$@ VL_DEBUG=1 VL_VLCOV=1 -f ../Makefile_obj 90 | 91 | ifneq ($(VERILATOR_NO_OPT_BUILD),1) # Faster laptop development... don't rebuild each commit 92 | ifneq ($(UNDER_GIT),) # If local git tree... else don't burden users 93 | GIT_CHANGE_DEP = ${srcdir}/../.git/logs/HEAD 94 | endif 95 | endif 96 | 97 | prefiles: : 98 | prefiles: : config_rev.h 99 | # This output goes into srcdir if locally configured, as we need to distribute it as part of the kit. 100 | config_rev.h: ${srcdir}/config_rev $(GIT_CHANGE_DEP) 101 | $(PYTHON3) ${srcdir}/config_rev ${srcdir} >$@ 102 | 103 | # Human convenience 104 | format: 105 | $(MAKE) -C .. $@ 106 | clang-format: 107 | $(MAKE) -C .. $@ 108 | 109 | maintainer-copy: : 110 | clean mostlyclean distclean maintainer-clean: : 111 | -rm -rf obj_* *.log *.dmp *.vpd core 112 | -rm -f *.o *.d *_gen_* 113 | -rm -f *__gen* obj_* 114 | -rm -f .objcache* 115 | 116 | distclean maintainer-clean: : 117 | -rm -f Makefile Makefile_obj config_package.h 118 | 119 | maintainer-clean: : 120 | -rm -f config_rev.h 121 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mbake" 7 | version = "1.4.3" 8 | description = "A Python-based Makefile formatter and linter" 9 | readme = "README.md" 10 | license = "MIT" 11 | requires-python = ">=3.9" 12 | authors = [ 13 | {name = "mbake Contributors"}, 14 | ] 15 | keywords = ["makefile", "formatter", "linter", "build-tools"] 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Environment :: Console", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Topic :: Software Development :: Build Tools", 28 | "Topic :: Software Development :: Quality Assurance", 29 | "Topic :: Text Processing :: Markup", 30 | ] 31 | dependencies = [ 32 | "typer >= 0.9.0", 33 | "rich >= 13.0.0", 34 | "tomli >= 1.2.1; python_version < '3.11'", 35 | ] 36 | 37 | [project.optional-dependencies] 38 | dev = [ 39 | "pytest >= 7.0.0", 40 | "pytest-cov >= 4.0.0", 41 | "black >= 23.0.0", 42 | "ruff >= 0.1.0", 43 | "mypy >= 1.0.0", 44 | "pre-commit >= 3.0.0", 45 | "tomli >= 1.2.1", # For type checking support 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/EbodShojaei/bake" 50 | Documentation = "https://github.com/EbodShojaei/bake#readme" 51 | Repository = "https://github.com/EbodShojaei/bake" 52 | "Bug Tracker" = "https://github.com/EbodShojaei/bake/issues" 53 | Changelog = "https://github.com/EbodShojaei/bake/releases" 54 | "Funding" = "https://github.com/sponsors/ebodshojaei" 55 | 56 | # Build-time configuration for command names 57 | # Set MBUILD_COMMAND_NAME environment variable to control which command(s) to install: 58 | # - "mbake" (default): Only install mbake command 59 | # - "bake": Only install bake command 60 | # - "both": Install both bake and mbake commands 61 | # - "auto": Let the build system choose based on environment 62 | 63 | [project.scripts] 64 | mbake = "mbake.cli:main" 65 | 66 | [tool.hatch.build.targets.wheel] 67 | packages = ["mbake"] 68 | include = [ 69 | "completions/**/*", 70 | ] 71 | 72 | # Build configuration for command name selection 73 | 74 | [tool.black] 75 | line-length = 88 76 | target-version = ['py39'] 77 | include = '\.pyi?$' 78 | extend-exclude = ''' 79 | /( 80 | # directories 81 | \.eggs 82 | | \.git 83 | | \.hg 84 | | \.mypy_cache 85 | | \.tox 86 | | \.venv 87 | | _build 88 | | buck-out 89 | | build 90 | | dist 91 | )/ 92 | ''' 93 | 94 | [tool.ruff.lint] 95 | select = [ 96 | "E", # pycodestyle 97 | "F", # pyflakes 98 | "UP", # pyupgrade 99 | "B", # flake8-bugbear 100 | "SIM", # flake8-simplify 101 | "I", # isort 102 | ] 103 | ignore = [ 104 | "E501", # line too long, handled by black 105 | "B008", # do not perform function calls in argument defaults 106 | ] 107 | 108 | [tool.ruff] 109 | line-length = 88 110 | target-version = "py39" 111 | 112 | [tool.ruff.lint.isort] 113 | known-first-party = ["bake"] 114 | 115 | [tool.mypy] 116 | python_version = "3.9" 117 | warn_return_any = true 118 | warn_unused_configs = true 119 | disallow_untyped_defs = true 120 | disallow_incomplete_defs = true 121 | check_untyped_defs = true 122 | disallow_untyped_decorators = true 123 | no_implicit_optional = true 124 | warn_redundant_casts = true 125 | warn_unused_ignores = true 126 | warn_no_return = true 127 | warn_unreachable = true 128 | strict_equality = true 129 | exclude = [ 130 | "tests/", 131 | ] 132 | 133 | [[tool.mypy.overrides]] 134 | module = "tests.*" 135 | ignore_errors = true 136 | 137 | [tool.pytest.ini_options] 138 | minversion = "7.0" 139 | addopts = "-ra -q --strict-markers --strict-config" 140 | testpaths = [ 141 | "tests", 142 | ] 143 | filterwarnings = [ 144 | "error", 145 | "ignore::UserWarning", 146 | "ignore::DeprecationWarning", 147 | ] 148 | 149 | [tool.coverage.run] 150 | source = ["bake"] 151 | branch = true 152 | 153 | [tool.coverage.report] 154 | exclude_lines = [ 155 | "pragma: no cover", 156 | "def __repr__", 157 | "if self.debug:", 158 | "if settings.DEBUG", 159 | "raise AssertionError", 160 | "raise NotImplementedError", 161 | "if 0:", 162 | "if __name__ == .__main__.:", 163 | "class .*\\bProtocol\\):", 164 | "@(abc\\.)?abstractmethod", 165 | ] -------------------------------------------------------------------------------- /tests/verilator/Makefile-3.txt: -------------------------------------------------------------------------------- 1 | #***************************************************************************** 2 | # 3 | # DESCRIPTION: Verilator Example: Makefile for inside source directory 4 | # 5 | # This calls the object directory makefile. That allows the objects to 6 | # be placed in the "current directory" which simplifies the Makefile. 7 | # 8 | # Copyright 2003-2025 by Wilson Snyder. This program is free software; you 9 | # can redistribute it and/or modify it under the terms of either the GNU 10 | # Lesser General Public License Version 3 or the Perl Artistic License 11 | # Version 2.0. 12 | # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 13 | # 14 | #****************************************************************************/ 15 | 16 | default: test 17 | 18 | # This must point to the root of the VERILATOR kit 19 | VERILATOR_ROOT ?= $(shell pwd)/.. 20 | export VERILATOR_ROOT 21 | 22 | # Pick up PERL and other variable settings 23 | include $(VERILATOR_ROOT)/include/verilated.mk 24 | 25 | ###################################################################### 26 | 27 | ifneq ($(VCS_HOME),) 28 | #Default to off, even with vcs; not all tests are ensured to be working 29 | #PRODUCTS += --vcs 30 | endif 31 | 32 | ifneq ($(NC_ROOT),) 33 | #Default to off, even with vcs; not all tests are ensured to be working 34 | #PRODUCTS += --nc 35 | endif 36 | 37 | # Run tests in parallel. 38 | ifeq ($(CFG_WITH_LONGTESTS),yes) 39 | DRIVER_FLAGS ?= -j 0 --quiet --rerun 40 | endif 41 | 42 | .SUFFIXES: 43 | 44 | ###################################################################### 45 | 46 | SCENARIOS ?= --vlt --vltmt --dist 47 | DRIVER_HASHSET ?= 48 | 49 | .PHONY: test 50 | test: 51 | $(PYTHON3) driver.py $(DRIVER_FLAGS) $(SCENARIOS) $(DRIVER_HASHSET) 52 | 53 | ###################################################################### 54 | 55 | vcs: 56 | $(PYTHON3) driver.py $(DRIVER_FLAGS) --vcs --stop 57 | 58 | ###################################################################### 59 | 60 | nc: 61 | $(PYTHON3) driver.py $(DRIVER_FLAGS) --nc --stop 62 | 63 | ###################################################################### 64 | 65 | vlt: 66 | $(PYTHON3) driver.py $(DRIVER_FLAGS) --vlt --stop 67 | 68 | vltmt: 69 | $(PYTHON3) driver.py $(DRIVER_FLAGS) --vltmt --stop 70 | 71 | ###################################################################### 72 | 73 | random: 74 | $(PYTHON3) driver.py $(DRIVER_FLAGS) --optimize : --stop 75 | 76 | random_forever: 77 | while ( VERILATOR_NO_DEBUG=1 CPPFLAGS_ADD=-Wno-error $(MAKE) random ) ; do \ 78 | echo ; \ 79 | done 80 | 81 | ####################################################################### 82 | # Informational - used by some tests 83 | 84 | print-cxx-version: 85 | $(CXX) --version 86 | 87 | ###################################################################### 88 | maintainer-copy:: 89 | clean mostlyclean distclean maintainer-clean:: 90 | -rm -rf obj_* simv* simx* csrc cov_work INCA_libs *.log *.key logs vc_hdrs.h 91 | -rm -rf t/obj_* t/__pycache__ 92 | 93 | distclean:: 94 | -rm -rf snapshot 95 | 96 | ###################################################################### 97 | # Generated code snapshot and diff for tests 98 | 99 | # Can be overridden for multiple snapshots 100 | TEST_SNAP_DIR ?= snapshot 101 | 102 | # Command to diff directories 103 | TEST_DIFF_TOOL ?= $(if $(shell which icdiff), icdiff -N -r, diff -r) 104 | 105 | TEST_SNAP_IGNORE := \ 106 | *.status *.log *.dat *.d *.o *.a *.so *stats*.txt *.html \ 107 | *.includecache *.out *.fst *.fst.vcd *.tree *.tree*.json \ 108 | *.dot *.csv *.xml *.hash *.cmake gmon.out.* CMakeFiles \ 109 | profile_exec.vcd t_pgo_threads *line-coverage*.txt \ 110 | profile.vlt *linkdot.txt *linkcells.txt *.log.sort *.vpp \ 111 | *.sarif 112 | 113 | define TEST_SNAP_template 114 | mkdir -p $(TEST_SNAP_DIR) 115 | rm -rf $(TEST_SNAP_DIR)/obj_$(1) 116 | cp -r obj_$(1) $(TEST_SNAP_DIR)/ 117 | find $(TEST_SNAP_DIR)/obj_$(1) \( $(TEST_SNAP_IGNORE:%=-name "%" -o) \ 118 | -type f -executable \) -prune | xargs rm -r 119 | endef 120 | 121 | .PHONY: test-snap 122 | test-snap: 123 | $(call TEST_SNAP_template,vlt) 124 | $(call TEST_SNAP_template,vltmt) 125 | $(call TEST_SNAP_template,dist) 126 | 127 | .PHONY: impl-test-diff 128 | impl-test-diff: 129 | $(TEST_DIFF_TOOL) $(TEST_SNAP_DIR)/obj_vlt obj_vlt || true 130 | $(TEST_DIFF_TOOL) $(TEST_SNAP_DIR)/obj_vltmt obj_vltmt || true 131 | $(TEST_DIFF_TOOL) $(TEST_SNAP_DIR)/obj_dist obj_dist || true 132 | 133 | .PHONY: test-diff 134 | test-diff: 135 | $(MAKE) impl-test-diff | grep -v "Only in obj_" \ 136 | | $$(git config --default less --global --get core.pager) 137 | -------------------------------------------------------------------------------- /mbake/constants/shell_commands.py: -------------------------------------------------------------------------------- 1 | """Shell command indicators for context-based detection.""" 2 | 3 | # POSIX shell built-ins and utilities - comprehensive list from The POSIX Shell And Utilities 4 | # See: https://shellhaters.org 5 | SHELL_COMMAND_INDICATORS = { 6 | # POSIX shell built-ins 7 | ":", 8 | ".", 9 | "alias", 10 | "bg", 11 | "break", 12 | "cd", 13 | "command", 14 | "continue", 15 | "eval", 16 | "exec", 17 | "exit", 18 | "export", 19 | "false", 20 | "fg", 21 | "getopts", 22 | "hash", 23 | "help", 24 | "history", 25 | "jobs", 26 | "kill", 27 | "let", 28 | "local", 29 | "pwd", 30 | "read", 31 | "readonly", 32 | "return", 33 | "set", 34 | "shift", 35 | "test", 36 | "times", 37 | "trap", 38 | "true", 39 | "type", 40 | "ulimit", 41 | "umask", 42 | "unalias", 43 | "unset", 44 | "wait", 45 | # Programming utilities 46 | "bc", 47 | "date", 48 | "env", 49 | "expr", 50 | "fc", 51 | "id", 52 | "locale", 53 | "localedef", 54 | "logger", 55 | "logname", 56 | "newgrp", 57 | "pathchk", 58 | "sh", 59 | "sleep", 60 | "time", 61 | "tput", 62 | "uname", 63 | "write", 64 | "xargs", 65 | # Text processing 66 | "awk", 67 | "cat", 68 | "cksum", 69 | "cmp", 70 | "comm", 71 | "csplit", 72 | "cut", 73 | "echo", 74 | "ed", 75 | "ex", 76 | "expand", 77 | "fold", 78 | "grep", 79 | "head", 80 | "join", 81 | "more", 82 | "nl", 83 | "od", 84 | "paste", 85 | "printf", 86 | "sed", 87 | "sort", 88 | "split", 89 | "tail", 90 | "tr", 91 | "tsort", 92 | "unexpand", 93 | "uniq", 94 | "wc", 95 | # File operations 96 | "basename", 97 | "cal", 98 | "chgrp", 99 | "chmod", 100 | "chown", 101 | "cp", 102 | "dd", 103 | "df", 104 | "dirname", 105 | "du", 106 | "file", 107 | "find", 108 | "fuser", 109 | "link", 110 | "ln", 111 | "ls", 112 | "mkdir", 113 | "mkfifo", 114 | "mv", 115 | "rm", 116 | "rmdir", 117 | "tee", 118 | "touch", 119 | "unlink", 120 | # Process management 121 | "nice", 122 | "nohup", 123 | "ps", 124 | "renice", 125 | # Job control 126 | "at", 127 | "batch", 128 | # Development tools 129 | "ar", 130 | "asa", 131 | "c99", 132 | "cflow", 133 | "ctags", 134 | "cxref", 135 | "diff", 136 | "fort77", 137 | "gencat", 138 | "iconv", 139 | "lex", 140 | "m4", 141 | "make", 142 | "man", 143 | "nm", 144 | "patch", 145 | "strings", 146 | "strip", 147 | "vi", 148 | "yacc", 149 | # System administration 150 | "crontab", 151 | "getconf", 152 | "who", 153 | # Terminal control 154 | "stty", 155 | "tabs", 156 | "tty", 157 | # Communication 158 | "mailx", 159 | "mesg", 160 | "talk", 161 | "uucp", 162 | "uudecode", 163 | "uuencode", 164 | "uustat", 165 | "uux", 166 | # Compression 167 | "compress", 168 | "pax", 169 | "uncompress", 170 | "zcat", 171 | } 172 | 173 | # Commands that NEVER create files with the target name 174 | # Only include POSIX-standard commands that output to stdout or perform actions 175 | # DO NOT include language runtimes (npm, python, node, etc.) - use structural detection instead 176 | NON_FILE_CREATING_COMMANDS = { 177 | # Text output commands (output to stdout, don't create files) 178 | "echo", 179 | "printf", 180 | "print", 181 | "cat", 182 | # Shell built-ins that don't create files 183 | "cd", 184 | "pwd", 185 | "export", 186 | "unset", 187 | "alias", 188 | "unalias", 189 | # Process/status commands 190 | "ps", 191 | "jobs", 192 | "kill", 193 | # Other non-file-creating POSIX commands 194 | "test", 195 | "[", # test command alias 196 | "true", 197 | "false", 198 | "sleep", 199 | "date", 200 | "uname", 201 | "who", 202 | "whoami", 203 | "id", 204 | } 205 | 206 | # Commands that ALWAYS create files when used with target name as argument 207 | # These commands create, copy, move, or link files 208 | # Used for phony target detection: if command is in this set and target is last arg, creates file 209 | FILE_CREATING_COMMANDS = { 210 | # File creation commands 211 | "touch", 212 | "mkdir", 213 | "mkfifo", 214 | # File copy/move commands 215 | "cp", 216 | "mv", 217 | "ln", 218 | "link", 219 | "install", 220 | # File transformation commands (when output is target) 221 | "dd", 222 | "tee", 223 | } 224 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mbake 2 | 3 | We love your input! We want to make contributing to mbake as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## Development Process 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ### Pull Requests 16 | 17 | 1. Fork the repo and create your branch from `main` 18 | 2. If you've added code that should be tested, add tests 19 | 3. If you've changed APIs, update the documentation 20 | 4. Ensure the test suite passes 21 | 5. Make sure your code lints 22 | 6. Issue that pull request! 23 | 24 | ### Development Setup 25 | 26 | ```bash 27 | # Clone your fork 28 | git clone https://github.com/ebodshojaei/bake.git 29 | cd mbake 30 | 31 | # Create virtual environment 32 | python -m venv venv 33 | source venv/bin/activate # On Windows: venv\Scripts\activate 34 | 35 | # Install in development mode 36 | pip install -e ".[dev]" 37 | 38 | # Install pre-commit hooks 39 | pre-commit install 40 | ``` 41 | 42 | ### Running Tests 43 | 44 | ```bash 45 | # Run all tests 46 | pytest 47 | 48 | # Run with coverage 49 | pytest --cov=bake --cov-report=html 50 | 51 | # Run specific test file 52 | pytest tests/test_bake.py -v 53 | 54 | # Run formatting tests 55 | pytest tests/test_comprehensive.py -v 56 | ``` 57 | 58 | ### Code Style 59 | 60 | We use several tools to maintain code quality: 61 | 62 | ```bash 63 | # Format code 64 | black bake tests 65 | 66 | # Sort imports 67 | ruff check --fix bake tests 68 | 69 | # Type checking 70 | mypy bake 71 | 72 | # Run all quality checks 73 | pre-commit run --all-files 74 | ``` 75 | 76 | ### Adding New Formatting Rules 77 | 78 | 1. Create a new rule in `bake/core/rules/` 79 | 2. Inherit from `FormatterPlugin` 80 | 3. Implement the `format` method 81 | 4. Add tests in `tests/fixtures/` 82 | 5. Update documentation 83 | 84 | Example: 85 | 86 | ```python 87 | from bake.plugins.base import FormatterPlugin, FormatResult 88 | 89 | class MyRule(FormatterPlugin): 90 | def __init__(self): 91 | super().__init__("my_rule", priority=50) 92 | 93 | def format(self, lines: List[str], config: dict) -> FormatResult: 94 | # Your formatting logic 95 | return FormatResult( 96 | lines=modified_lines, 97 | changed=True, 98 | errors=[], 99 | warnings=[] 100 | ) 101 | ``` 102 | 103 | ### Testing Your Changes 104 | 105 | Always add tests for new functionality: 106 | 107 | ```bash 108 | # Create test fixtures 109 | mkdir tests/fixtures/my_feature 110 | echo "input content" > tests/fixtures/my_feature/input.mk 111 | echo "expected output" > tests/fixtures/my_feature/expected.mk 112 | 113 | # Add test case 114 | # Edit tests/test_comprehensive.py to include your test 115 | ``` 116 | 117 | ## Any contributions you make will be under the MIT Software License 118 | 119 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](LICENSE) that covers the project. 120 | 121 | ## Report bugs using GitHub's issue tracker 122 | 123 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/ebodshojaei/bake/issues). 124 | 125 | **Great Bug Reports** tend to have: 126 | 127 | - A quick summary and/or background 128 | - Steps to reproduce 129 | - Be specific! 130 | - Give sample code if you can 131 | - What you expected would happen 132 | - What actually happens 133 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 134 | 135 | ## Feature Requests 136 | 137 | We welcome feature requests! Please open an issue with: 138 | 139 | - Clear description of the feature 140 | - Why it would be useful 141 | - Example use cases 142 | - Proposed implementation (if you have ideas) 143 | 144 | ## Code of Conduct 145 | 146 | ### Our Pledge 147 | 148 | We pledge to make participation in our project a harassment-free experience for everyone. 149 | 150 | ### Our Standards 151 | 152 | Examples of behavior that contributes to creating a positive environment include: 153 | 154 | - Using welcoming and inclusive language 155 | - Being respectful of differing viewpoints and experiences 156 | - Gracefully accepting constructive criticism 157 | - Focusing on what is best for the community 158 | - Showing empathy towards other community members 159 | 160 | ### Enforcement 161 | 162 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated. 163 | 164 | ## License 165 | 166 | By contributing, you agree that your contributions will be licensed under its MIT License. 167 | -------------------------------------------------------------------------------- /tests/fixtures/real_world_complex/input.mk: -------------------------------------------------------------------------------- 1 | # Real-world complex Makefile example 2 | # Project: Example C++ Application with multiple components 3 | .PHONY: all clean debug distclean docs format help install lint profile release test uninstall 4 | 5 | # Build configuration 6 | DEBUG ?= 0 7 | PROFILE ?= 0 8 | STATIC ?= 0 9 | 10 | # Toolchain detection 11 | UNAME_S := $(shell uname -s) 12 | ifeq ($(UNAME_S),Linux) 13 | PLATFORM = linux 14 | CC = gcc 15 | CXX = g++ 16 | else ifeq ($(UNAME_S),Darwin) 17 | PLATFORM = macos 18 | CC = clang 19 | CXX = clang++ 20 | else 21 | $(error Unsupported platform: $(UNAME_S)) 22 | endif 23 | 24 | # Version information 25 | VERSION = $(shell git describe --tags --dirty --always 2>/dev/null || echo "unknown") 26 | BUILD_DATE = $(shell date +'%Y-%m-%d %H: %M: %S') 27 | COMMIT = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") 28 | 29 | # Directory structure 30 | SRCDIR = src 31 | INCDIR = include 32 | BUILDDIR = build 33 | BINDIR = bin 34 | LIBDIR = lib 35 | TESTDIR = tests 36 | 37 | # Source files discovery 38 | SOURCES = $(wildcard $(SRCDIR)/*.cpp) \ 39 | $(wildcard $(SRCDIR)/*/*.cpp) \ 40 | $(wildcard $(SRCDIR)/*/*/*.cpp) 41 | HEADERS = $(wildcard $(INCDIR)/*.h) $(wildcard $(INCDIR)/*.hpp) 42 | TEST_SOURCES = $(wildcard $(TESTDIR)/*.cpp) 43 | 44 | # Object files 45 | OBJECTS = $(SOURCES:$(SRCDIR)/%.cpp=$(BUILDDIR)/%.o) 46 | TEST_OBJECTS = $(TEST_SOURCES:$(TESTDIR)/%.cpp=$(BUILDDIR)/test_%.o) 47 | 48 | # Binary names 49 | TARGET = $(BINDIR)/myapp 50 | TEST_TARGET = $(BINDIR)/test_runner 51 | LIBRARY = $(LIBDIR)/libmyapp.a 52 | 53 | # Compiler flags with conditional settings 54 | CPPFLAGS = -I$(INCDIR) -DVERSION=\"$(VERSION)\" -DBUILD_DATE=\"$(BUILD_DATE)\" 55 | CXXFLAGS = -std=c++17 -Wall -Wextra -Wpedantic 56 | 57 | ifeq ($(DEBUG),1) 58 | CXXFLAGS += -g -O0 -DDEBUG 59 | BUILDDIR := $(BUILDDIR)/debug 60 | else 61 | CXXFLAGS += -O3 -DNDEBUG 62 | BUILDDIR := $(BUILDDIR)/release 63 | endif 64 | 65 | ifeq ($(PROFILE),1) 66 | CXXFLAGS += -pg 67 | LDFLAGS += -pg 68 | endif 69 | 70 | ifeq ($(STATIC),1) 71 | LDFLAGS += -static 72 | endif 73 | 74 | # Library dependencies 75 | LIBS = -lpthread -lm 76 | ifeq ($(PLATFORM),linux) 77 | LIBS += -ldl -lrt 78 | endif 79 | 80 | # Phony targets declaration 81 | 82 | # Default target 83 | all: $(TARGET) 84 | 85 | # Main executable 86 | $(TARGET): $(OBJECTS) | $(BINDIR) 87 | $(CXX) $(OBJECTS) -o $@ $(LDFLAGS) $(LIBS) 88 | 89 | # Static library 90 | $(LIBRARY): $(OBJECTS) | $(LIBDIR) 91 | ar rcs $@ $^ 92 | ranlib $@ 93 | 94 | # Object files compilation 95 | $(BUILDDIR)/%.o: $(SRCDIR)/%.cpp | $(BUILDDIR) 96 | @mkdir -p $(dir $@) 97 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ 98 | 99 | # Test object files 100 | $(BUILDDIR)/test_%.o: $(TESTDIR)/%.cpp | $(BUILDDIR) 101 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ 102 | 103 | # Test executable 104 | test: $(TEST_TARGET) 105 | $(TEST_TARGET) 106 | 107 | $(TEST_TARGET): $(TEST_OBJECTS) $(LIBRARY) | $(BINDIR) 108 | $(CXX) $(TEST_OBJECTS) -L$(LIBDIR) -lmyapp -o $@ $(LDFLAGS) $(LIBS) 109 | 110 | # Directory creation 111 | $(BUILDDIR) $(BINDIR) $(LIBDIR): 112 | @mkdir -p $@ 113 | 114 | # Convenience targets 115 | debug: 116 | $(MAKE) DEBUG=1 117 | 118 | release: 119 | $(MAKE) DEBUG=0 120 | 121 | profile: 122 | $(MAKE) PROFILE=1 123 | 124 | # Installation 125 | PREFIX ?= /usr/local 126 | DESTDIR ?= 127 | 128 | install: $(TARGET) 129 | install -d $(DESTDIR)$(PREFIX)/bin 130 | install -m755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/ 131 | install -d $(DESTDIR)$(PREFIX)/share/man/man1/ 132 | install -m644 docs/$(notdir $(TARGET)).1 $(DESTDIR)$(PREFIX)/share/man/man1/ 133 | 134 | uninstall: 135 | rm -f $(DESTDIR)$(PREFIX)/bin/$(notdir $(TARGET)) 136 | rm -f $(DESTDIR)$(PREFIX)/share/man/man1/$(notdir $(TARGET)).1 137 | 138 | # Development tools 139 | format: 140 | find $(SRCDIR) $(INCDIR) $(TESTDIR) -name "*.cpp" -o -name "*.h" -o -name "*.hpp" | \ 141 | xargs clang-format -i 142 | 143 | lint: 144 | cppcheck --enable=all --std=c++17 --suppress=missingIncludeSystem \ 145 | $(SRCDIR) $(INCDIR) $(TESTDIR) 146 | 147 | docs: 148 | doxygen Doxyfile 149 | 150 | # Cleanup 151 | clean: 152 | rm -rf $(BUILDDIR) $(BINDIR) $(LIBDIR) 153 | 154 | distclean: clean 155 | rm -rf docs/html docs/latex 156 | 157 | # Help target 158 | help: 159 | @echo "Available targets:" 160 | @echo " all - Build the main executable (default)" 161 | @echo " test - Build and run tests" 162 | @echo " clean - Remove build artifacts" 163 | @echo " install - Install the application" 164 | @echo " format - Format source code" 165 | @echo " lint - Run static analysis" 166 | @echo "" 167 | @echo "Configuration options:" 168 | @echo " DEBUG=1 - Build with debug symbols" 169 | @echo " STATIC=1 - Build static binary" 170 | 171 | # Dependency tracking 172 | -include $(OBJECTS:.o=.d) 173 | -include $(TEST_OBJECTS:.o=.d) 174 | 175 | # Generate dependency files 176 | $(BUILDDIR)/%.d: $(SRCDIR)/%.cpp | $(BUILDDIR) 177 | @mkdir -p $(dir $@) 178 | $(CXX) $(CPPFLAGS) -MM -MT $(BUILDDIR)/$*.o $< > $@ 179 | -------------------------------------------------------------------------------- /tests/fixtures/real_world_complex/expected.mk: -------------------------------------------------------------------------------- 1 | # Real-world complex Makefile example 2 | # Project: Example C++ Application with multiple components 3 | .PHONY: all clean debug distclean docs format help install lint profile release test uninstall 4 | 5 | # Build configuration 6 | DEBUG ?= 0 7 | PROFILE ?= 0 8 | STATIC ?= 0 9 | 10 | # Toolchain detection 11 | UNAME_S := $(shell uname -s) 12 | ifeq ($(UNAME_S),Linux) 13 | PLATFORM = linux 14 | CC = gcc 15 | CXX = g++ 16 | else ifeq ($(UNAME_S),Darwin) 17 | PLATFORM = macos 18 | CC = clang 19 | CXX = clang++ 20 | else 21 | $(error Unsupported platform: $(UNAME_S)) 22 | endif 23 | 24 | # Version information 25 | VERSION = $(shell git describe --tags --dirty --always 2>/dev/null || echo "unknown") 26 | BUILD_DATE = $(shell date +'%Y-%m-%d %H: %M: %S') 27 | COMMIT = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") 28 | 29 | # Directory structure 30 | SRCDIR = src 31 | INCDIR = include 32 | BUILDDIR = build 33 | BINDIR = bin 34 | LIBDIR = lib 35 | TESTDIR = tests 36 | 37 | # Source files discovery 38 | SOURCES = $(wildcard $(SRCDIR)/*.cpp) \ 39 | $(wildcard $(SRCDIR)/*/*.cpp) \ 40 | $(wildcard $(SRCDIR)/*/*/*.cpp) 41 | HEADERS = $(wildcard $(INCDIR)/*.h) $(wildcard $(INCDIR)/*.hpp) 42 | TEST_SOURCES = $(wildcard $(TESTDIR)/*.cpp) 43 | 44 | # Object files 45 | OBJECTS = $(SOURCES:$(SRCDIR)/%.cpp=$(BUILDDIR)/%.o) 46 | TEST_OBJECTS = $(TEST_SOURCES:$(TESTDIR)/%.cpp=$(BUILDDIR)/test_%.o) 47 | 48 | # Binary names 49 | TARGET = $(BINDIR)/myapp 50 | TEST_TARGET = $(BINDIR)/test_runner 51 | LIBRARY = $(LIBDIR)/libmyapp.a 52 | 53 | # Compiler flags with conditional settings 54 | CPPFLAGS = -I$(INCDIR) -DVERSION=\"$(VERSION)\" -DBUILD_DATE=\"$(BUILD_DATE)\" 55 | CXXFLAGS = -std=c++17 -Wall -Wextra -Wpedantic 56 | 57 | ifeq ($(DEBUG),1) 58 | CXXFLAGS += -g -O0 -DDEBUG 59 | BUILDDIR := $(BUILDDIR)/debug 60 | else 61 | CXXFLAGS += -O3 -DNDEBUG 62 | BUILDDIR := $(BUILDDIR)/release 63 | endif 64 | 65 | ifeq ($(PROFILE),1) 66 | CXXFLAGS += -pg 67 | LDFLAGS += -pg 68 | endif 69 | 70 | ifeq ($(STATIC),1) 71 | LDFLAGS += -static 72 | endif 73 | 74 | # Library dependencies 75 | LIBS = -lpthread -lm 76 | ifeq ($(PLATFORM),linux) 77 | LIBS += -ldl -lrt 78 | endif 79 | 80 | # Phony targets declaration 81 | 82 | # Default target 83 | all: $(TARGET) 84 | 85 | # Main executable 86 | $(TARGET): $(OBJECTS) | $(BINDIR) 87 | $(CXX) $(OBJECTS) -o $@ $(LDFLAGS) $(LIBS) 88 | 89 | # Static library 90 | $(LIBRARY): $(OBJECTS) | $(LIBDIR) 91 | ar rcs $@ $^ 92 | ranlib $@ 93 | 94 | # Object files compilation 95 | $(BUILDDIR)/%.o: $(SRCDIR)/%.cpp | $(BUILDDIR) 96 | @mkdir -p $(dir $@) 97 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ 98 | 99 | # Test object files 100 | $(BUILDDIR)/test_%.o: $(TESTDIR)/%.cpp | $(BUILDDIR) 101 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ 102 | 103 | # Test executable 104 | test: $(TEST_TARGET) 105 | $(TEST_TARGET) 106 | 107 | $(TEST_TARGET): $(TEST_OBJECTS) $(LIBRARY) | $(BINDIR) 108 | $(CXX) $(TEST_OBJECTS) -L$(LIBDIR) -lmyapp -o $@ $(LDFLAGS) $(LIBS) 109 | 110 | # Directory creation 111 | $(BUILDDIR) $(BINDIR) $(LIBDIR): 112 | @mkdir -p $@ 113 | 114 | # Convenience targets 115 | debug: 116 | $(MAKE) DEBUG=1 117 | 118 | release: 119 | $(MAKE) DEBUG=0 120 | 121 | profile: 122 | $(MAKE) PROFILE=1 123 | 124 | # Installation 125 | PREFIX ?= /usr/local 126 | DESTDIR ?= 127 | 128 | install: $(TARGET) 129 | install -d $(DESTDIR)$(PREFIX)/bin 130 | install -m755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/ 131 | install -d $(DESTDIR)$(PREFIX)/share/man/man1/ 132 | install -m644 docs/$(notdir $(TARGET)).1 $(DESTDIR)$(PREFIX)/share/man/man1/ 133 | 134 | uninstall: 135 | rm -f $(DESTDIR)$(PREFIX)/bin/$(notdir $(TARGET)) 136 | rm -f $(DESTDIR)$(PREFIX)/share/man/man1/$(notdir $(TARGET)).1 137 | 138 | # Development tools 139 | format: 140 | find $(SRCDIR) $(INCDIR) $(TESTDIR) -name "*.cpp" -o -name "*.h" -o -name "*.hpp" | \ 141 | xargs clang-format -i 142 | 143 | lint: 144 | cppcheck --enable=all --std=c++17 --suppress=missingIncludeSystem \ 145 | $(SRCDIR) $(INCDIR) $(TESTDIR) 146 | 147 | docs: 148 | doxygen Doxyfile 149 | 150 | # Cleanup 151 | clean: 152 | rm -rf $(BUILDDIR) $(BINDIR) $(LIBDIR) 153 | 154 | distclean: clean 155 | rm -rf docs/html docs/latex 156 | 157 | # Help target 158 | help: 159 | @echo "Available targets:" 160 | @echo " all - Build the main executable (default)" 161 | @echo " test - Build and run tests" 162 | @echo " clean - Remove build artifacts" 163 | @echo " install - Install the application" 164 | @echo " format - Format source code" 165 | @echo " lint - Run static analysis" 166 | @echo "" 167 | @echo "Configuration options:" 168 | @echo " DEBUG=1 - Build with debug symbols" 169 | @echo " STATIC=1 - Build static binary" 170 | 171 | # Dependency tracking 172 | -include $(OBJECTS:.o=.d) 173 | -include $(TEST_OBJECTS:.o=.d) 174 | 175 | # Generate dependency files 176 | $(BUILDDIR)/%.d: $(SRCDIR)/%.cpp | $(BUILDDIR) 177 | @mkdir -p $(dir $@) 178 | $(CXX) $(CPPFLAGS) -MM -MT $(BUILDDIR)/$*.o $< > $@ 179 | -------------------------------------------------------------------------------- /mbake/core/rules/special_target_validation.py: -------------------------------------------------------------------------------- 1 | """Special target validation and formatting rule for Makefiles.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | 8 | 9 | class SpecialTargetValidationRule(FormatterPlugin): 10 | """Validates special target usage and syntax.""" 11 | 12 | def __init__(self) -> None: 13 | super().__init__("special_target_validation", priority=10) 14 | 15 | def format( 16 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 17 | ) -> FormatResult: 18 | """Validate special target declarations.""" 19 | formatted_lines = [] 20 | changed = False 21 | errors: list[str] = [] 22 | warnings: list[str] = [] 23 | 24 | special_targets = self._get_special_targets() 25 | target_usage = self._analyze_special_target_usage(lines) 26 | 27 | for _i, line in enumerate(lines): 28 | stripped = line.strip() 29 | 30 | if stripped.startswith(".") and ":" in stripped: 31 | new_line, line_errors, line_warnings = self._validate_special_target( 32 | line, special_targets, target_usage 33 | ) 34 | if new_line != line: 35 | changed = True 36 | errors.extend(line_errors) 37 | warnings.extend(line_warnings) 38 | formatted_lines.append(new_line) 39 | else: 40 | formatted_lines.append(line) 41 | 42 | return FormatResult( 43 | lines=formatted_lines, 44 | changed=changed, 45 | errors=errors, 46 | warnings=warnings, 47 | check_messages=[], 48 | ) 49 | 50 | def _get_special_targets(self) -> dict[str, dict[str, Any]]: 51 | """Get information about special targets.""" 52 | return { 53 | ".PHONY": {"duplicatable": True, "requires_prereqs": True}, 54 | ".SUFFIXES": {"duplicatable": True, "requires_prereqs": False}, 55 | ".DEFAULT": {"duplicatable": False, "requires_prereqs": False}, 56 | ".PRECIOUS": {"duplicatable": True, "requires_prereqs": False}, 57 | ".INTERMEDIATE": {"duplicatable": True, "requires_prereqs": False}, 58 | ".SECONDARY": {"duplicatable": True, "requires_prereqs": False}, 59 | ".IGNORE": {"duplicatable": True, "requires_prereqs": True}, 60 | ".SILENT": {"duplicatable": True, "requires_prereqs": True}, 61 | ".POSIX": {"duplicatable": False, "requires_prereqs": False}, 62 | ".NOTPARALLEL": {"duplicatable": False, "requires_prereqs": False}, 63 | ".ONESHELL": {"duplicatable": False, "requires_prereqs": False}, 64 | ".EXPORT_ALL_VARIABLES": {"duplicatable": False, "requires_prereqs": False}, 65 | ".LOW_RESOLUTION_TIME": {"duplicatable": False, "requires_prereqs": False}, 66 | ".SECOND_EXPANSION": {"duplicatable": False, "requires_prereqs": False}, 67 | ".SECONDEXPANSION": {"duplicatable": False, "requires_prereqs": False}, 68 | ".VARIABLES": {"duplicatable": False, "requires_prereqs": False}, 69 | ".MAKE": {"duplicatable": False, "requires_prereqs": False}, 70 | ".WAIT": {"duplicatable": False, "requires_prereqs": False}, 71 | ".INCLUDE_DIRS": {"duplicatable": False, "requires_prereqs": False}, 72 | ".LIBPATTERNS": {"duplicatable": False, "requires_prereqs": False}, 73 | } 74 | 75 | def _analyze_special_target_usage(self, lines: list[str]) -> dict[str, int]: 76 | """Analyze how many times each special target is used.""" 77 | usage: dict[str, int] = {} 78 | 79 | for line in lines: 80 | stripped = line.strip() 81 | if stripped.startswith(".") and ":" in stripped: 82 | target_name = stripped.split(":")[0] 83 | usage[target_name] = usage.get(target_name, 0) + 1 84 | 85 | return usage 86 | 87 | def _validate_special_target( 88 | self, line: str, special_targets: dict, target_usage: dict 89 | ) -> tuple[str, list[str], list[str]]: 90 | """Validate a special target declaration.""" 91 | errors: list[str] = [] 92 | warnings: list[str] = [] 93 | 94 | # Extract target name 95 | match = re.match(r"^(\.[A-Z_]+):", line) 96 | if not match: 97 | return line, errors, warnings 98 | 99 | target_name = match.group(1) 100 | 101 | if target_name not in special_targets: 102 | errors.append(f"Unknown special target '{target_name}'") 103 | return line, errors, warnings 104 | 105 | target_info = special_targets[target_name] 106 | 107 | # Check if target is used multiple times when it shouldn't be 108 | if not target_info["duplicatable"] and target_usage.get(target_name, 0) > 1: 109 | errors.append( 110 | f"Special target '{target_name}' cannot be declared multiple times" 111 | ) 112 | 113 | # Check if prerequisites are required 114 | content = line[line.find(":") + 1 :].strip() 115 | if target_info["requires_prereqs"] and not content: 116 | warnings.append( 117 | f"Special target '{target_name}' typically requires prerequisites" 118 | ) 119 | 120 | return line, errors, warnings 121 | -------------------------------------------------------------------------------- /mbake/core/rules/assignment_spacing.py: -------------------------------------------------------------------------------- 1 | """Assignment operator spacing rule for Makefiles.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | from ...utils.pattern_utils import PatternUtils 8 | 9 | 10 | class AssignmentSpacingRule(FormatterPlugin): 11 | """Handles spacing around assignment operators (=, :=, +=, ?=).""" 12 | 13 | def __init__(self) -> None: 14 | super().__init__("assignment_spacing", priority=15) 15 | 16 | def format( 17 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 18 | ) -> FormatResult: 19 | """Normalize spacing around assignment operators.""" 20 | formatted_lines = [] 21 | changed = False 22 | errors: list[str] = [] 23 | warnings: list[str] = [] 24 | 25 | space_around_assignment = config.get("space_around_assignment", True) 26 | 27 | for i, line in enumerate(lines, 1): 28 | new_line = line # Default to original line 29 | 30 | # Skip recipe lines (lines starting with tab) or comments or empty lines 31 | if ( 32 | line.startswith("\t") 33 | or line.strip().startswith("#") 34 | or not line.strip() 35 | ): 36 | pass # Keep original line 37 | # Check if line contains assignment operator 38 | elif re.match(r"^[A-Za-z_][A-Za-z0-9_]*\s*(:=|\+=|\?=|=|!=)\s*", line): 39 | # Check for reversed assignment operators first 40 | reversed_warning = self._check_reversed_operators(line, i) 41 | if reversed_warning: 42 | warnings.append(reversed_warning) 43 | 44 | # Skip substitution references like $(VAR:pattern=replacement) which are not assignments 45 | # or skip invalid target-like lines of the form VAR=token:... (no space after '=') 46 | if re.search( 47 | r"\$\([^)]*:[^)]*=[^)]*\)", line 48 | ) or self._is_invalid_target_syntax(line): 49 | pass # Keep original line 50 | else: 51 | # Extract the parts - be more careful about the operator 52 | match = re.match( 53 | r"^([A-Za-z_][A-Za-z0-9_]*)\s*(:=|\+=|\?=|=|!=)\s*(.*)", line 54 | ) 55 | if match: 56 | var_name = match.group(1) 57 | operator = match.group(2) 58 | value = match.group(3) 59 | 60 | # Only format if this is actually an assignment (not a target) 61 | if operator in ["=", ":=", "?=", "+=", "!="]: 62 | if space_around_assignment: 63 | # Only add trailing space if there's actually a value 64 | if value.strip(): 65 | new_line = f"{var_name} {operator} {value}" 66 | else: 67 | new_line = f"{var_name} {operator}" 68 | else: 69 | new_line = f"{var_name}{operator}{value}" 70 | 71 | # Single append at the end 72 | if new_line != line: 73 | changed = True 74 | formatted_lines.append(new_line) 75 | 76 | return FormatResult( 77 | lines=formatted_lines, 78 | changed=changed, 79 | errors=errors, 80 | warnings=warnings, 81 | check_messages=[], 82 | ) 83 | 84 | def _is_invalid_target_syntax(self, line: str) -> bool: 85 | """Check if line contains invalid target syntax that should be preserved.""" 86 | stripped = line.strip() 87 | # Only flag when there is NO whitespace after '=' before the first ':' 88 | # This avoids flagging typical assignments whose values contain ':' (URLs, times, paths). 89 | if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*=\S*:\S*", stripped): 90 | return False 91 | # Allow colon-safe values to pass (URLs, datetimes, drive/path patterns) 92 | after_eq = stripped.split("=", 1)[1] 93 | return not PatternUtils.value_is_colon_safe(after_eq) 94 | 95 | def _check_reversed_operators(self, line: str, line_number: int) -> str: 96 | """Check for reversed assignment operators and return warning message if found.""" 97 | stripped = line.strip() 98 | 99 | # Skip if this is not a variable assignment line 100 | if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*\s*=", stripped): 101 | return "" 102 | 103 | # Check for reversed operators: =?, =:, =+ 104 | # Pattern: variable = [space] [?:+] [space or end or value] 105 | # This handles both "FOO = ? bar" and "FOO =?bar" cases 106 | reversed_match = re.search(r"=\s*([?:+])(?:\s|$|[a-zA-Z0-9_])", stripped) 107 | if reversed_match: 108 | reversed_char = reversed_match.group(1) 109 | correct_operator = f"{reversed_char}=" 110 | reversed_operator = f"={reversed_char}" 111 | 112 | # Get the variable name for context 113 | var_match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)", stripped) 114 | var_name = var_match.group(1) if var_match else "variable" 115 | 116 | return ( 117 | f"Line {line_number}: Possible typo in assignment operator '{reversed_operator}' " 118 | f"for variable '{var_name}', did you mean '{correct_operator}'? " 119 | f"Make will treat this as a regular assignment with '{reversed_char}' as part of the value." 120 | ) 121 | 122 | return "" 123 | -------------------------------------------------------------------------------- /vscode-mbake-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mbake-makefile-formatter", 3 | "displayName": "mbake Makefile Formatter", 4 | "description": "Format Makefiles using the mbake formatter", 5 | "version": "1.4.0", 6 | "publisher": "eshojaei", 7 | "engines": { 8 | "vscode": "^1.60.0" 9 | }, 10 | "categories": [ 11 | "Formatters", 12 | "Other" 13 | ], 14 | "keywords": [ 15 | "makefile", 16 | "formatter", 17 | "mbake", 18 | "build", 19 | "make" 20 | ], 21 | "activationEvents": [ 22 | "onLanguage:makefile" 23 | ], 24 | "main": "./extension.js", 25 | "contributes": { 26 | "languages": [ 27 | { 28 | "id": "makefile", 29 | "aliases": [ 30 | "Makefile", 31 | "makefile" 32 | ], 33 | "extensions": [ 34 | ".mk", 35 | ".make" 36 | ], 37 | "filenames": [ 38 | "Makefile", 39 | "makefile", 40 | "GNUmakefile" 41 | ] 42 | } 43 | ], 44 | "commands": [ 45 | { 46 | "command": "mbake.formatMakefile", 47 | "title": "Format Makefile", 48 | "category": "mbake" 49 | }, 50 | { 51 | "command": "mbake.checkMakefile", 52 | "title": "Check Makefile Formatting", 53 | "category": "mbake" 54 | }, 55 | { 56 | "command": "mbake.initConfig", 57 | "title": "Initialize Configuration", 58 | "category": "mbake" 59 | } 60 | ], 61 | "menus": { 62 | "editor/context": [ 63 | { 64 | "command": "mbake.formatMakefile", 65 | "when": "resourceLangId == makefile", 66 | "group": "1_modification" 67 | }, 68 | { 69 | "command": "mbake.checkMakefile", 70 | "when": "resourceLangId == makefile", 71 | "group": "1_modification" 72 | } 73 | ], 74 | "commandPalette": [ 75 | { 76 | "command": "mbake.formatMakefile", 77 | "when": "resourceLangId == makefile" 78 | }, 79 | { 80 | "command": "mbake.checkMakefile", 81 | "when": "resourceLangId == makefile" 82 | }, 83 | { 84 | "command": "mbake.initConfig" 85 | } 86 | ] 87 | }, 88 | "keybindings": [ 89 | { 90 | "command": "mbake.formatMakefile", 91 | "key": "shift+alt+f", 92 | "when": "resourceLangId == makefile && editorTextFocus" 93 | } 94 | ], 95 | "configuration": { 96 | "title": "mbake", 97 | "properties": { 98 | "mbake.executablePath": { 99 | "type": [ 100 | "string", 101 | "array" 102 | ], 103 | "default": "mbake", 104 | "description": "Path to the mbake executable. Can be a string (e.g., 'mbake', 'bake', '/path/to/mbake') or an array for custom execution (e.g., ['python', '-m', 'mbake'] or ['nix-shell', '-p', 'mbake', '--run', 'mbake']). Use 'mbake' if in your PATH, or provide the full path." 105 | }, 106 | "mbake.configPath": { 107 | "type": "string", 108 | "default": "", 109 | "description": "Path to the bake configuration file. Leave empty to use default (~/.bake.toml)." 110 | }, 111 | "mbake.formatOnSave": { 112 | "type": "boolean", 113 | "default": false, 114 | "description": "Automatically format Makefiles on save." 115 | }, 116 | "mbake.showDiff": { 117 | "type": "boolean", 118 | "default": false, 119 | "description": "Show diff of changes when formatting." 120 | }, 121 | "mbake.verbose": { 122 | "type": "boolean", 123 | "default": false, 124 | "description": "Enable verbose output." 125 | }, 126 | "mbake.autoInit": { 127 | "type": "boolean", 128 | "default": true, 129 | "description": "Automatically offer to initialize configuration when missing." 130 | } 131 | } 132 | } 133 | }, 134 | "scripts": { 135 | "vscode:prepublish": "npm run compile", 136 | "compile": "echo 'No compilation needed for JavaScript'", 137 | "watch": "echo 'No watch needed for JavaScript'", 138 | "package": "vsce package" 139 | }, 140 | "devDependencies": { 141 | "@types/node": "^16.x", 142 | "@types/vscode": "^1.60.0", 143 | "@vscode/vsce": "^3.5.0" 144 | }, 145 | "repository": { 146 | "type": "git", 147 | "url": "https://github.com/ebodshojaei/bake.git", 148 | "directory": "vscode-mbake-extension" 149 | }, 150 | "homepage": "https://github.com/ebodshojaei/bake#readme", 151 | "bugs": { 152 | "url": "https://github.com/ebodshojaei/bake/issues" 153 | }, 154 | "license": "MIT", 155 | "icon": "icon.png", 156 | "galleryBanner": { 157 | "color": "#1e1e1e", 158 | "theme": "dark" 159 | } 160 | } -------------------------------------------------------------------------------- /tests/test_validate_command.py: -------------------------------------------------------------------------------- 1 | """Tests for the validate command.""" 2 | 3 | import tempfile 4 | from pathlib import Path 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | from typer.testing import CliRunner 9 | 10 | from mbake.cli import app 11 | 12 | 13 | class TestValidateCommand: 14 | """Test the validate command functionality.""" 15 | 16 | @pytest.fixture 17 | def runner(self): 18 | """Create a CLI runner for testing.""" 19 | return CliRunner() 20 | 21 | def test_validate_simple_makefile(self, runner): 22 | """Test validation of a simple Makefile.""" 23 | with tempfile.NamedTemporaryFile(mode="w", suffix=".mk", delete=False) as f: 24 | f.write("test_target:\n\techo 'hello'\n") 25 | makefile_path = f.name 26 | 27 | try: 28 | with patch("subprocess.run") as mock_run: 29 | mock_run.return_value.returncode = 0 30 | mock_run.return_value.stderr = "" 31 | 32 | result = runner.invoke(app, ["validate", makefile_path]) 33 | 34 | assert result.exit_code == 0 35 | assert "Valid" in result.stdout 36 | assert "syntax" in result.stdout 37 | mock_run.assert_called_once() 38 | 39 | # Check that the command was called with the correct arguments 40 | call_args = mock_run.call_args 41 | assert call_args[0][0] == [ 42 | "make", 43 | "-f", 44 | Path(makefile_path).name, 45 | "--dry-run", 46 | "--just-print", 47 | ] 48 | assert call_args[1]["cwd"] == Path(makefile_path).parent 49 | finally: 50 | Path(makefile_path).unlink() 51 | 52 | def test_validate_makefile_with_relative_include(self, runner): 53 | """Test validation of a Makefile with relative include paths.""" 54 | # Create a temporary directory structure 55 | with tempfile.TemporaryDirectory() as temp_dir: 56 | temp_path = Path(temp_dir) 57 | 58 | # Create a common Makefile in the parent directory 59 | common_mk = temp_path / "common.mk" 60 | common_mk.write_text("COMMON_VAR = 'from common'\n") 61 | 62 | # Create a subdirectory 63 | subdir = temp_path / "subdir" 64 | subdir.mkdir() 65 | 66 | # Create a Makefile in the subdirectory that includes the common file 67 | makefile_mk = subdir / "Makefile" 68 | makefile_mk.write_text( 69 | "include ../common.mk\ntest_target:\n\techo $(COMMON_VAR)\n" 70 | ) 71 | 72 | with patch("subprocess.run") as mock_run: 73 | mock_run.return_value.returncode = 0 74 | mock_run.return_value.stderr = "" 75 | 76 | # Test validation from parent directory 77 | result = runner.invoke(app, ["validate", str(makefile_mk)]) 78 | 79 | assert result.exit_code == 0 80 | assert "Valid" in result.stdout 81 | assert "syntax" in result.stdout 82 | mock_run.assert_called_once() 83 | 84 | # Check that the command was called with the correct working directory 85 | call_args = mock_run.call_args 86 | assert call_args[1]["cwd"] == subdir 87 | assert call_args[0][0] == [ 88 | "make", 89 | "-f", 90 | "Makefile", 91 | "--dry-run", 92 | "--just-print", 93 | ] 94 | 95 | def test_validate_makefile_with_syntax_error(self, runner): 96 | """Test validation of a Makefile with syntax errors.""" 97 | with tempfile.NamedTemporaryFile(mode="w", suffix=".mk", delete=False) as f: 98 | f.write("test_target:\necho 'missing tab'\n") # Missing tab 99 | makefile_path = f.name 100 | 101 | try: 102 | result = runner.invoke(app, ["validate", makefile_path]) 103 | 104 | # The actual behavior is exit code 2 for syntax errors 105 | assert result.exit_code == 2 106 | assert "Invalid" in result.stdout 107 | assert "syntax" in result.stdout 108 | finally: 109 | Path(makefile_path).unlink() 110 | 111 | def test_validate_nonexistent_file(self, runner): 112 | """Test validation of a non-existent file.""" 113 | result = runner.invoke(app, ["validate", "nonexistent.mk"]) 114 | 115 | assert result.exit_code == 2 116 | assert "File not found" in result.stdout 117 | 118 | def test_validate_multiple_files(self, runner): 119 | """Test validation of multiple files.""" 120 | with ( 121 | tempfile.NamedTemporaryFile(mode="w", suffix=".mk", delete=False) as f1, 122 | tempfile.NamedTemporaryFile(mode="w", suffix=".mk", delete=False) as f2, 123 | ): 124 | f1.write("target1:\n\techo 'hello'\n") 125 | f2.write("target2:\n\techo 'world'\n") 126 | makefile1_path = f1.name 127 | makefile2_path = f2.name 128 | 129 | try: 130 | with patch("subprocess.run") as mock_run: 131 | mock_run.return_value.returncode = 0 132 | mock_run.return_value.stderr = "" 133 | 134 | result = runner.invoke( 135 | app, ["validate", makefile1_path, makefile2_path] 136 | ) 137 | 138 | assert result.exit_code == 0 139 | assert "Valid" in result.stdout 140 | assert "syntax" in result.stdout 141 | assert mock_run.call_count == 2 142 | finally: 143 | Path(makefile1_path).unlink() 144 | Path(makefile2_path).unlink() 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍞 mbake 2 | 3 | 4 |
5 | mbake logo 6 |
7 | A Makefile formatter and linter. It only took 50 years! 8 |

9 | 10 | License: MIT 11 | 12 | 13 | Python 3.9+ 14 | 15 | 16 | PyPI - mbake 17 | 18 | 19 | Code style: black 20 | 21 | 22 | PyPI Downloads 23 | 24 |
25 | 26 | 27 | ## Features 28 | 29 | - **Smart formatting**: Tabs for recipes, consistent spacing, line continuation cleanup 30 | - **Intelligent .PHONY detection**: Automatically identifies and manages phony targets 31 | - **Syntax validation**: Ensures Makefiles are valid before and after formatting 32 | - **Configurable rules**: Customize behavior via `~/.bake.toml` 33 | - **CI/CD ready**: Check mode for automated formatting validation 34 | - **VSCode extension**: Full editor integration available 35 | 36 | ## Installation 37 | 38 | ```bash 39 | # Install from PyPI 40 | pip install mbake 41 | 42 | # Or install VSCode extension 43 | # Search for "mbake Makefile Formatter" in VSCode Extensions 44 | ``` 45 | 46 | ## Quick Start 47 | 48 | ```bash 49 | # Format a Makefile 50 | mbake format Makefile 51 | 52 | # Check if formatting is needed (CI/CD mode) 53 | mbake format --check Makefile 54 | 55 | # Validate Makefile syntax 56 | mbake validate Makefile 57 | 58 | # Initialize configuration 59 | mbake init 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### Basic Commands 65 | 66 | ```bash 67 | # Format files 68 | mbake format Makefile # Format single file 69 | mbake format --check Makefile # Check formatting (CI/CD) 70 | mbake format --diff Makefile # Show changes without modifying 71 | 72 | # Validate syntax 73 | mbake validate Makefile # Check Makefile syntax with GNU make 74 | 75 | # Configuration 76 | mbake init # Create config file 77 | mbake config # Show current settings 78 | mbake update # Update to latest version 79 | ``` 80 | 81 | ### Key Options 82 | 83 | - `--check`: Check formatting without making changes (perfect for CI/CD) 84 | - `--diff`: Show what changes would be made 85 | - `--backup`: Create backup before formatting 86 | - `--validate`: Validate syntax after formatting 87 | - `--config`: Use custom configuration file 88 | 89 | ## Configuration 90 | 91 | Create a config file with `mbake init`. Key settings: 92 | 93 | ```toml 94 | [formatter] 95 | # Spacing and formatting 96 | space_around_assignment = true # CC=gcc to CC = gcc 97 | space_after_colon = true # myapp.o:myapp.c to myapp.o: myapp.c 98 | normalize_line_continuations = true # Clean up backslash continuations 99 | remove_trailing_whitespace = true # Remove trailing spaces 100 | fix_missing_recipe_tabs = true # Convert spaces to tabs in recipes 101 | 102 | # .PHONY management 103 | auto_insert_phony_declarations = false # Auto-detect and insert .PHONY 104 | group_phony_declarations = false # Group multiple .PHONY lines 105 | phony_at_top = false # Place .PHONY at file top 106 | ``` 107 | 108 | ## Smart .PHONY Detection 109 | 110 | mbake intelligently detects phony targets by analyzing recipe commands: 111 | 112 | ```makefile 113 | # These are automatically detected as phony 114 | test: 115 | npm test 116 | 117 | clean: 118 | rm -f *.o 119 | 120 | deploy: 121 | ssh user@server 'systemctl restart app' 122 | 123 | # This creates a file, so it's NOT phony 124 | myapp.o: myapp.c 125 | gcc -c myapp.c -o myapp.o 126 | ``` 127 | 128 | Enable auto-insertion in your config: 129 | 130 | ```toml 131 | [formatter] 132 | auto_insert_phony_declarations = true 133 | ``` 134 | 135 | ## Examples 136 | 137 | ### Before and After 138 | 139 | ```makefile 140 | # Before: Inconsistent spacing and indentation 141 | CC:=gcc 142 | CFLAGS= -Wall -g 143 | SOURCES=main.c \ 144 | utils.c \ 145 | helper.c 146 | 147 | all: $(TARGET) 148 | $(CC) $(CFLAGS) -o $@ $^ 149 | 150 | clean: 151 | rm -f *.o 152 | ``` 153 | 154 | 155 | ```makefile 156 | # After: Clean, consistent formatting 157 | CC := gcc 158 | CFLAGS = -Wall -g 159 | SOURCES = main.c \ 160 | utils.c \ 161 | helper.c 162 | 163 | all: $(TARGET) 164 | $(CC) $(CFLAGS) -o $@ $^ 165 | 166 | clean: 167 | rm -f *.o 168 | ``` 169 | 170 | 171 | ### Disable Formatting 172 | 173 | Use special comments to disable formatting in specific regions: 174 | 175 | ```makefile 176 | # bake-format off 177 | CUSTOM_FORMAT= \ 178 | 1 \ 179 | 45678 \ 180 | #bake-format on 181 | ``` 182 | 183 | ## CI/CD Integration 184 | 185 | ```yaml 186 | # GitHub Actions example 187 | - name: Check Makefile formatting 188 | run: | 189 | pip install mbake 190 | mbake format --check Makefile 191 | ``` 192 | 193 | Exit codes: `0` (success), `1` (needs formatting), `2` (error) 194 | 195 | ## Contributing 196 | 197 | Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. 198 | 199 | ```bash 200 | # Development setup 201 | git clone https://github.com/ebodshojaei/bake.git 202 | cd mbake 203 | pip install -e ".[dev]" 204 | pytest # Run tests 205 | ``` 206 | 207 | ## License 208 | 209 | MIT License - see [LICENSE](LICENSE) for details. 210 | -------------------------------------------------------------------------------- /mbake/core/rules/duplicate_targets.py: -------------------------------------------------------------------------------- 1 | """Rule for detecting duplicate targets in Makefiles.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | from ...utils.line_utils import ConditionalTracker 8 | 9 | 10 | class DuplicateTargetRule(FormatterPlugin): 11 | """Detects duplicate target definitions.""" 12 | 13 | def __init__(self) -> None: 14 | super().__init__("duplicate_targets", priority=5) 15 | 16 | def format( 17 | self, 18 | lines: list[str], 19 | config: dict[str, Any], 20 | check_mode: bool = False, 21 | **context: Any, 22 | ) -> FormatResult: 23 | """Format lines by detecting duplicate targets.""" 24 | errors = self._detect_duplicates(lines, config) 25 | # This rule doesn't modify content, just reports errors 26 | return FormatResult( 27 | lines=lines, changed=False, errors=errors, warnings=[], check_messages=[] 28 | ) 29 | 30 | def _detect_duplicates(self, lines: list[str], config: dict[str, Any]) -> list[str]: 31 | """Detect duplicate target definitions.""" 32 | target_pattern = re.compile(r"^([^:\s]+):(:?)\s*(.*)$") 33 | seen_targets: dict[str, tuple[int, str, tuple]] = ( 34 | {} 35 | ) # Added conditional context 36 | errors = [] 37 | 38 | # Track conditional context 39 | conditional_tracker = ConditionalTracker() 40 | 41 | # Special targets that can be duplicated 42 | declarative_targets = {".PHONY", ".SUFFIXES"} 43 | 44 | for i, line in enumerate(lines): 45 | stripped = line.strip() 46 | line_num = i + 1 47 | 48 | # Get conditional context for this line 49 | conditional_context = conditional_tracker.process_line(line, i) 50 | 51 | # Skip empty lines, comments, and recipes (must check original line for tab) 52 | if not stripped or stripped.startswith("#") or line.startswith("\t"): 53 | continue 54 | 55 | # Check for target definitions 56 | match = target_pattern.match(stripped) 57 | if match: 58 | target_name = match.group(1).strip() 59 | is_double_colon = match.group(2) == ":" 60 | target_body = match.group(3).strip() 61 | 62 | # Skip comment-only targets (documentation targets) 63 | # These are lines like "target: ## Comment" that are documentation only 64 | if target_body.startswith("##"): 65 | continue 66 | 67 | # Skip special targets that can be duplicated 68 | if target_name in declarative_targets: 69 | continue 70 | 71 | # Suppress duplicate errors for template placeholder targets like $(1), $(2) 72 | if self._is_template_placeholder(target_name): 73 | continue 74 | 75 | # Double-colon rules are allowed to have multiple definitions 76 | if is_double_colon: 77 | continue 78 | 79 | # Check for previous definition 80 | if target_name in seen_targets: 81 | prev_line, prev_body, prev_context = seen_targets[target_name] 82 | 83 | # Check if targets are in mutually exclusive conditional branches 84 | if ConditionalTracker.are_mutually_exclusive( 85 | prev_context, conditional_context 86 | ): 87 | # Targets in mutually exclusive branches are not duplicates 88 | seen_targets[target_name] = ( 89 | line_num, 90 | target_body, 91 | conditional_context, 92 | ) 93 | continue 94 | 95 | # Check if this is a static pattern rule (contains %) 96 | # Static pattern rules can coexist with other rules for the same target 97 | is_static_pattern = "%" in target_body 98 | prev_is_static_pattern = "%" in prev_body 99 | 100 | # If either rule is a static pattern rule, they can coexist 101 | if is_static_pattern or prev_is_static_pattern: 102 | continue 103 | 104 | # Check if this is a target-specific variable assignment 105 | # Pattern: "target: VARIABLE += value" or "target: VARIABLE = value" 106 | is_var_assignment = bool( 107 | re.match(r"^\s*[A-Z_][A-Z0-9_]*\s*[+:?]?=", target_body) 108 | ) 109 | prev_is_var_assignment = bool( 110 | re.match(r"^\s*[A-Z_][A-Z0-9_]*\s*[+:?]?=", prev_body) 111 | ) 112 | 113 | if is_var_assignment or prev_is_var_assignment: 114 | # This looks like target-specific variable assignments, which are valid 115 | continue 116 | 117 | # Format error message 118 | message = f"Duplicate target '{target_name}' defined at lines {prev_line} and {line_num}. Second definition will override the first." 119 | error_msg = self._format_error_message(message, line_num, config) 120 | errors.append(error_msg) 121 | # Record target with conditional context 122 | seen_targets[target_name] = (line_num, target_body, conditional_context) 123 | 124 | return errors 125 | 126 | def _is_template_placeholder(self, target_name: str) -> bool: 127 | """Check if target is a template placeholder like $(1), $(2), $(VAR), etc.""" 128 | return bool(re.fullmatch(r"\$[({][^})]+[})]", target_name)) 129 | 130 | def _format_error_message(self, message: str, line_num: int, config: dict) -> str: 131 | """Format error message according to configuration.""" 132 | gnu_error_format = config.get("_global", {}).get("gnu_error_format", True) 133 | 134 | if gnu_error_format: 135 | return f"{line_num}: Error: {message}" 136 | else: 137 | return f"Error: {message} (line {line_num})" 138 | -------------------------------------------------------------------------------- /mbake/core/rules/suffix_validation.py: -------------------------------------------------------------------------------- 1 | """Suffix rule validation and formatting rule for Makefiles.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...constants.makefile_targets import DEFAULT_SUFFIXES 7 | from ...plugins.base import FormatResult, FormatterPlugin 8 | 9 | 10 | class SuffixValidationRule(FormatterPlugin): 11 | """Validates and formats suffix rules and .SUFFIXES declarations.""" 12 | 13 | def __init__(self) -> None: 14 | super().__init__("suffix_validation", priority=15) 15 | 16 | def format( 17 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 18 | ) -> FormatResult: 19 | """Validate suffix rules and .SUFFIXES declarations.""" 20 | formatted_lines = [] 21 | changed = False 22 | errors: list[str] = [] 23 | warnings: list[str] = [] 24 | 25 | declared_suffixes: set[str] = set() 26 | 27 | for _i, line in enumerate(lines): 28 | stripped = line.strip() 29 | 30 | # Validate .SUFFIXES declarations 31 | if stripped.startswith(".SUFFIXES:"): 32 | new_line, line_errors, line_warnings = ( 33 | self._validate_suffixes_declaration(line, declared_suffixes) 34 | ) 35 | if new_line != line: 36 | changed = True 37 | errors.extend(line_errors) 38 | warnings.extend(line_warnings) 39 | formatted_lines.append(new_line) 40 | 41 | # Update declared suffixes with new ones from this line 42 | content = line[line.find(":") + 1 :].strip() 43 | if content: 44 | new_suffixes = [s for s in content.split() if s.startswith(".")] 45 | declared_suffixes.update(new_suffixes) 46 | 47 | # Validate suffix rules 48 | elif self._is_suffix_rule_line(stripped): 49 | line_errors, line_warnings = self._validate_suffix_rule( 50 | line, declared_suffixes 51 | ) 52 | errors.extend(line_errors) 53 | warnings.extend(line_warnings) 54 | formatted_lines.append(line) 55 | 56 | else: 57 | formatted_lines.append(line) 58 | 59 | return FormatResult( 60 | lines=formatted_lines, 61 | changed=changed, 62 | errors=errors, 63 | warnings=warnings, 64 | check_messages=[], 65 | ) 66 | 67 | def _is_suffix_rule_line(self, line: str) -> bool: 68 | """Check if line defines a suffix rule.""" 69 | # Pattern: .suffix1.suffix2: (no prerequisites for suffix rules) 70 | return bool(re.match(r"^\.[^:]+\.\w+:\s*$", line)) 71 | 72 | def _validate_suffix_rule( 73 | self, line: str, declared_suffixes: set[str] 74 | ) -> tuple[list[str], list[str]]: 75 | """Validate a suffix rule.""" 76 | errors: list[str] = [] 77 | warnings: list[str] = [] 78 | 79 | # Extract target (e.g., .a.b) 80 | match = re.match(r"^(\.[^:]+\.\w+):", line) 81 | if not match: 82 | return errors, warnings 83 | 84 | target = match.group(1) 85 | parts = target.split(".") 86 | if len(parts) != 3: 87 | return errors, warnings 88 | 89 | suffix1 = "." + parts[1] 90 | suffix2 = "." + parts[2] 91 | 92 | # Check if suffixes are declared 93 | if suffix1 not in declared_suffixes: 94 | errors.append(f"Suffix rule '{target}' uses undeclared suffix '{suffix1}'") 95 | 96 | if suffix2 not in declared_suffixes: 97 | errors.append(f"Suffix rule '{target}' uses undeclared suffix '{suffix2}'") 98 | 99 | return errors, warnings 100 | 101 | def _validate_suffixes_declaration( 102 | self, line: str, declared_suffixes: set[str] 103 | ) -> tuple[str, list[str], list[str]]: 104 | """Validate a .SUFFIXES declaration.""" 105 | errors: list[str] = [] 106 | warnings: list[str] = [] 107 | 108 | # Extract suffixes from .SUFFIXES: .a .b .c 109 | content = line[line.find(":") + 1 :].strip() 110 | 111 | if content: 112 | suffixes = content.split() 113 | new_suffixes = set() 114 | 115 | for suffix in suffixes: 116 | # Validate suffix format 117 | if not suffix.startswith("."): 118 | errors.append( 119 | f"Invalid suffix '{suffix}' - suffixes must start with '.'" 120 | ) 121 | continue 122 | 123 | # Check for duplicate declarations 124 | if suffix in declared_suffixes: 125 | warnings.append( 126 | f"Suffix '{suffix}' is already declared in previous .SUFFIXES statement" 127 | ) 128 | 129 | # Check for duplicates within this declaration 130 | if suffix in new_suffixes: 131 | errors.append( 132 | f"Duplicate suffix '{suffix}' in .SUFFIXES declaration" 133 | ) 134 | 135 | new_suffixes.add(suffix) 136 | 137 | # Check for unusual suffix patterns 138 | for suffix in new_suffixes: 139 | if len(suffix) < 2: # Just "." or very short 140 | warnings.append( 141 | f"Unusual suffix '{suffix}' - consider if this is intentional" 142 | ) 143 | elif suffix.count(".") > 1: # Multiple dots like ".tar.gz" 144 | warnings.append( 145 | f"Complex suffix '{suffix}' - ensure this is supported by your Make version" 146 | ) 147 | 148 | return line, errors, warnings 149 | 150 | def _get_declared_suffixes(self, all_lines: list[str]) -> set[str]: 151 | """Extract suffixes declared in .SUFFIXES statements.""" 152 | suffixes = set() 153 | 154 | for line in all_lines: 155 | stripped = line.strip() 156 | if stripped.startswith(".SUFFIXES:"): 157 | # Parse suffixes from .SUFFIXES: .a .b .c 158 | content = stripped[9:].strip() # Remove '.SUFFIXES:' 159 | if content: # If not empty (which clears all suffixes) 160 | suffixes.update(content.split()) 161 | 162 | # If no .SUFFIXES found, use default suffixes 163 | if not suffixes: 164 | suffixes = DEFAULT_SUFFIXES 165 | 166 | return suffixes 167 | -------------------------------------------------------------------------------- /mbake/core/rules/shell.py: -------------------------------------------------------------------------------- 1 | """Shell script formatting rule for Makefile recipes.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from ...plugins.base import FormatResult, FormatterPlugin 7 | 8 | 9 | class ShellFormattingRule(FormatterPlugin): 10 | """Handles proper indentation of shell scripts within recipe lines.""" 11 | 12 | def __init__(self) -> None: 13 | super().__init__("shell_formatting", priority=50) 14 | 15 | def format( 16 | self, lines: list[str], config: dict, check_mode: bool = False, **context: Any 17 | ) -> FormatResult: 18 | """Format shell script indentation within recipes.""" 19 | formatted_lines = [] 20 | changed = False 21 | errors: list[str] = [] 22 | warnings: list[str] = [] 23 | 24 | i = 0 25 | while i < len(lines): 26 | line = lines[i] 27 | 28 | # Check if this is a recipe line 29 | if line.startswith("\t") and line.strip(): 30 | # Look for shell control structures 31 | stripped = line.lstrip("\t ") 32 | 33 | # Check if this starts a shell control structure 34 | if self._is_shell_control_start(stripped): 35 | # Process the shell block 36 | shell_block, block_end = self._extract_shell_block(lines, i) 37 | formatted_block = self._format_shell_block(shell_block) 38 | 39 | if formatted_block != shell_block: 40 | changed = True 41 | 42 | formatted_lines.extend(formatted_block) 43 | i = block_end 44 | else: 45 | formatted_lines.append(line) 46 | i += 1 47 | else: 48 | formatted_lines.append(line) 49 | i += 1 50 | 51 | return FormatResult( 52 | lines=formatted_lines, 53 | changed=changed, 54 | errors=errors, 55 | warnings=warnings, 56 | check_messages=[], 57 | ) 58 | 59 | def _is_shell_control_start(self, line: str) -> bool: 60 | """Check if a line starts a shell control structure.""" 61 | # Strip make command prefixes (@, -, +) 62 | stripped = line.lstrip("@-+ ") 63 | 64 | # More precise matching than just startswith, to avoid matching substrings 65 | control_patterns = [ 66 | r"^if\s+\[", 67 | r"^for\s+", 68 | r"^while\s+", 69 | r"^case\s+", 70 | r"^until\s+", 71 | r"^{\s*$", 72 | ] 73 | return any(re.match(pattern, stripped) for pattern in control_patterns) 74 | 75 | def _extract_shell_block( 76 | self, lines: list[str], start_idx: int 77 | ) -> tuple[list[str], int]: 78 | """Extract a shell control block from lines.""" 79 | block = [] 80 | i = start_idx 81 | 82 | while i < len(lines): 83 | line = lines[i] 84 | block.append(line) 85 | 86 | # If line doesn't end with continuation, this might be the end 87 | if not line.rstrip().endswith("\\"): 88 | i += 1 89 | break 90 | 91 | # Check for control structure end markers 92 | stripped = line.lstrip("\t ") 93 | if any( 94 | stripped.strip().startswith(end) for end in ["fi", "done", "esac", "}"] 95 | ): 96 | i += 1 97 | break 98 | 99 | i += 1 100 | 101 | return block, i 102 | 103 | def _format_shell_block(self, block: list[str]) -> list[str]: 104 | """Format a shell control block with proper indentation.""" 105 | if not block: 106 | return block 107 | 108 | formatted = [] 109 | indent_level = 0 110 | 111 | # Shell control structure keywords 112 | start_keywords = ("if", "for", "while", "case", "until") 113 | continuation_keywords = ("elif", "else") 114 | end_keywords = ("fi", "done", "esac") 115 | 116 | # Determine the base indentation level from the first line 117 | # This preserves the original indentation level instead of forcing it to 1 tab 118 | base_tabs = 1 # Default fallback 119 | if block and block[0].startswith("\t"): 120 | # Count leading tabs in the first line 121 | base_tabs = 0 122 | for char in block[0]: 123 | if char == "\t": 124 | base_tabs += 1 125 | else: 126 | break 127 | if base_tabs == 0: 128 | base_tabs = 1 # Fallback if no tabs found 129 | 130 | for line in block: 131 | if not line.strip(): 132 | formatted.append(line) 133 | continue 134 | 135 | # Preserve the original line ending (including any trailing spaces) 136 | line_content = line.rstrip("\n\r") 137 | stripped = line_content.lstrip("\t ") 138 | 139 | # Check for trailing spaces/content after the main command 140 | trailing = "" 141 | if line_content.endswith(" "): 142 | # Count trailing spaces 143 | trailing_spaces = len(line_content) - len(line_content.rstrip(" ")) 144 | trailing = " " * trailing_spaces 145 | stripped = stripped.rstrip(" ") 146 | 147 | # Strip make command prefixes for keyword detection 148 | command_content = stripped.lstrip("@-+ ") 149 | 150 | # Adjust indent level for closing keywords 151 | if any( 152 | command_content.strip().startswith(kw) 153 | for kw in continuation_keywords + end_keywords 154 | ): 155 | indent_level = max(0, indent_level - 1) 156 | 157 | # Calculate proper indentation, preserving base indentation level 158 | if indent_level == 0: 159 | # Primary recipe level - use base tabs from original indentation 160 | new_line = "\t" * base_tabs + stripped + trailing 161 | else: 162 | # Nested shell level - add extra tabs instead of mixing tabs and spaces 163 | new_line = "\t" * (base_tabs + indent_level) + stripped + trailing 164 | 165 | formatted.append(new_line) 166 | 167 | # Adjust indent level for opening keywords 168 | if any( 169 | command_content.strip().startswith(kw) 170 | for kw in start_keywords + continuation_keywords 171 | ) and stripped.rstrip().endswith("\\"): 172 | indent_level += 1 173 | 174 | return formatted 175 | --------------------------------------------------------------------------------