Skip to content

[CHORE]: Add make lint filename|dirname target to Makefile #410

@crivetimihai

Description

@crivetimihai

[CHORE]: Add file/directory-specific linting support to Makefile

🔧 Chore Description

Priority: High (Developer Experience)

Description:
Add support for linting specific files or directories by passing them as arguments to make lint. This allows developers to quickly check only the files they're working on without running linters across the entire codebase.

Additional: Option to only run make lint on the staged files from a commit (or anything that's modified), and a way to run this as part of a pre-commit hook.

📋 Current Behavior

$ make lint
# Runs ALL linters on ENTIRE codebase (slow)

$ make lint mcpgateway/server.py
# Error: No rule to make target 'mcpgateway/server.py'

✅ Expected Behavior

$ make lint mcpgateway/server.py
# Runs all applicable linters on just that file

$ make lint mcpgateway/auth/
# Runs all linters on just that directory

$ make lint
# Still works as before (entire codebase)

🛠️ Implementation

1. Add new TARGET variable and lint-target:

# =============================================================================
# 🔍 LINTING & STATIC ANALYSIS
# =============================================================================

# Allow specific file/directory targeting
TARGET ?= mcpgateway tests

# Individual linter flags for file/directory support
ISORT_TARGET = $(TARGET)
BLACK_TARGET = $(TARGET)
FLAKE8_TARGET = $(TARGET)
PYLINT_TARGET = $(TARGET)
MYPY_TARGET = $(TARGET)
BANDIT_TARGET = $(if $(filter mcpgateway tests,$(TARGET)),-r $(TARGET),$(TARGET))
RUFF_TARGET = $(TARGET)
PYRIGHT_TARGET = $(TARGET)

# List of linters that support file/directory targeting
FILE_AWARE_LINTERS := isort black flake8 pylint mypy bandit ruff pyright \
                      pydocstyle pycodestyle

# Master lint target with file/directory support
.PHONY: lint
lint:
	@if [ "$(MAKECMDGOALS)" = "lint" ] && [ -n "$(filter-out lint,$(MAKECMDGOALS))" ]; then \
		TARGET="$(filter-out lint,$(MAKECMDGOALS))"; \
		echo "🔍 Linting specific target: $$TARGET"; \
		$(MAKE) lint-target TARGET="$$TARGET"; \
	else \
		echo "🔍 Running full lint suite on: $(TARGET)"; \
		$(MAKE) lint-all TARGET="$(TARGET)"; \
	fi

# Handle file/directory as a target
%:
	@if [ "$(firstword $(MAKECMDGOALS))" = "lint" ] && [ -e "$@" ]; then \
		true; \
	else \
		echo "Error: Unknown target '$@'"; \
		exit 1; \
	fi

.PHONY: lint-target
lint-target:
	@echo "🎯 Linting $(TARGET)..."
	@for linter in $(FILE_AWARE_LINTERS); do \
		if command -v $(VENV_DIR)/bin/$$linter >/dev/null 2>&1 || [ "$$linter" = "pre-commit" ]; then \
			echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
			echo "- $$linter on $(TARGET)"; \
			$(MAKE) $$linter-file TARGET="$(TARGET)" || true; \
		fi; \
	done

.PHONY: lint-all
lint-all:
	@set -e; for t in $(LINTERS); do \
		echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
		echo "- $$t"; \
		$(MAKE) $$t TARGET="$(TARGET)" || true; \
	done

2. Update individual linter targets to support TARGET:

# --- Python formatters/linters with file support ---

isort-file isort:
	@echo "🔀 isort $(TARGET)..."
	@$(VENV_DIR)/bin/isort $(TARGET)

black-file black:
	@echo "🎨 black $(TARGET)..."
	@$(VENV_DIR)/bin/black -l 200 $(TARGET)

flake8-file flake8:
	@echo "🐍 flake8 $(TARGET)..."
	@$(VENV_DIR)/bin/flake8 $(TARGET)

pylint-file pylint:
	@echo "🐛 pylint $(TARGET)..."
	@if [ -f "$(TARGET)" ]; then \
		$(VENV_DIR)/bin/pylint $(TARGET); \
	else \
		$(VENV_DIR)/bin/pylint $(TARGET); \
	fi

mypy-file mypy:
	@echo "🏷️ mypy $(TARGET)..."
	@$(VENV_DIR)/bin/mypy $(TARGET)

bandit-file bandit:
	@echo "🛡️ bandit $(TARGET)..."
	@if [ -d "$(TARGET)" ]; then \
		$(VENV_DIR)/bin/bandit -r $(TARGET); \
	else \
		$(VENV_DIR)/bin/bandit $(TARGET); \
	fi

ruff-file ruff:
	@echo "⚡ ruff $(TARGET)..."
	@$(VENV_DIR)/bin/ruff check $(TARGET)
	@$(VENV_DIR)/bin/ruff format --check $(TARGET)

pyright-file pyright:
	@echo "🏷️ pyright $(TARGET)..."
	@$(VENV_DIR)/bin/pyright $(TARGET)

pydocstyle-file pydocstyle:
	@echo "📚 pydocstyle $(TARGET)..."
	@$(VENV_DIR)/bin/pydocstyle $(TARGET)

pycodestyle-file pycodestyle:
	@echo "📝 pycodestyle $(TARGET)..."
	@$(VENV_DIR)/bin/pycodestyle --max-line-length=200 $(TARGET)

3. Add convenience targets for common use cases:

# Convenience targets for specific file types
.PHONY: lint-py lint-yaml lint-json lint-md

# Lint only Python files in target
lint-py:
	@if [ -f "$(TARGET)" ]; then \
		echo "🐍 Linting Python file: $(TARGET)"; \
		$(MAKE) lint-target TARGET="$(TARGET)"; \
	else \
		echo "🐍 Linting Python files in: $(TARGET)"; \
		find $(TARGET) -name "*.py" -type f | while read f; do \
			$(MAKE) lint-target TARGET="$$f"; \
		done; \
	fi

# Quick lint - only fast linters
lint-quick:
	@echo "⚡ Quick lint of $(TARGET) (ruff + black + isort)..."
	@$(MAKE) ruff-file TARGET="$(TARGET)"
	@$(MAKE) black-file TARGET="$(TARGET)" ARGS="--check --diff"
	@$(MAKE) isort-file TARGET="$(TARGET)" ARGS="--check-only"

# Fix formatting issues in target
lint-fix:
	@echo "🔧 Fixing lint issues in $(TARGET)..."
	@$(MAKE) black-file TARGET="$(TARGET)"
	@$(MAKE) isort-file TARGET="$(TARGET)"
	@$(MAKE) ruff-file TARGET="$(TARGET)" ARGS="--fix"

4. Handle file type detection:

# Smart linting based on file extension
.PHONY: lint-smart
lint-smart:
	@for target in $(TARGET); do \
		if [ ! -e "$$target" ]; then \
			echo "❌ File/directory not found: $$target"; \
			continue; \
		fi; \
		case "$$target" in \
			*.py) \
				echo "🐍 Python file detected: $$target"; \
				$(MAKE) lint-py TARGET="$$target" ;; \
			*.yaml|*.yml) \
				echo "📄 YAML file detected: $$target"; \
				$(MAKE) yamllint TARGET="$$target" ;; \
			*.json) \
				echo "📄 JSON file detected: $$target"; \
				$(MAKE) jsonlint TARGET="$$target" ;; \
			*.md) \
				echo "📝 Markdown file detected: $$target"; \
				$(MAKE) markdownlint TARGET="$$target" ;; \
			*.toml) \
				echo "📄 TOML file detected: $$target"; \
				$(MAKE) tomllint TARGET="$$target" ;; \
			*) \
				if [ -d "$$target" ]; then \
					echo "📁 Directory detected: $$target"; \
					$(MAKE) lint-target TARGET="$$target"; \
				else \
					echo "❓ Unknown file type, running all applicable linters"; \
					$(MAKE) lint-target TARGET="$$target"; \
				fi ;; \
		esac; \
	done

📋 Usage Examples

# Lint a specific file
make lint mcpgateway/server.py

# Lint a directory
make lint mcpgateway/auth/

# Lint multiple files
make lint mcpgateway/server.py mcpgateway/config.py

# Quick lint (fast checks only)
make lint-quick mcpgateway/server.py

# Fix formatting issues
make lint-fix mcpgateway/server.py

# Smart lint (auto-detect file type)
make lint-smart config.yaml

# Traditional full project lint
make lint

# Lint with specific tool only
make black mcpgateway/server.py
make flake8 mcpgateway/auth/

🧪 Testing Requirements

# Test 1: Single file
make lint mcpgateway/__init__.py

# Test 2: Directory
make lint mcpgateway/auth/

# Test 3: Non-existent file
make lint nonexistent.py  # Should error gracefully

# Test 4: Multiple targets
make lint mcpgateway/*.py

# Test 5: Different file types
make lint README.md
make lint pyproject.toml

# Test 6: Quick lint
make lint-quick mcpgateway/

# Test 7: Fix mode
make lint-fix mcpgateway/server.py

✅ Acceptance Criteria

  • make lint filename works for single files
  • make lint dirname works for directories
  • make lint without args still lints entire project
  • All Python linters support file/directory targeting
  • Config file linters detect file type and run appropriately
  • Clear error messages for non-existent files
  • make lint-fix repairs formatting issues
  • make lint-quick provides fast feedback
  • Performance: Single file linting is notably faster than full project

🔧 Implementation Notes

  1. Linter Compatibility:

    • Most Python linters accept files/directories directly
    • Some tools may need special handling (e.g., mypy with imports)
    • Config linters need file-type detection
  2. Performance Considerations:

    • Avoid re-running slow linters unnecessarily
    • Consider caching for incremental linting
    • Quick-lint mode for rapid development
  3. Error Handling:

    • Gracefully handle non-existent files
    • Continue on linter failures (don't stop the pipeline)
    • Clear indication of which linter failed

📚 Additional Features (Future)

# Watch mode - lint on file changes
lint-watch:
	@echo "👁️ Watching $(TARGET) for changes..."
	@watchmedo shell-command \
		--patterns="*.py" \
		--recursive \
		--command='make lint-quick ${watch_src_path}' \
		$(TARGET)

# Lint only changed files (git)
lint-changed:
	@echo "🔍 Linting changed files..."
	@git diff --name-only --diff-filter=ACM | grep -E '\.(py|yaml|yml|json|md|toml)$$' | \
		xargs -I {} $(MAKE) lint-smart TARGET={}

# Lint with error threshold
lint-strict:
	@$(MAKE) lint TARGET="$(TARGET)" | tee lint-report.txt
	@errors=$$(grep -c "error:" lint-report.txt || true); \
	if [ $$errors -gt 0 ]; then \
		echo "❌ Found $$errors errors"; \
		exit 1; \
	fi

Benefits:

  • Faster feedback loop during development
  • Ability to check files before committing
  • Reduced CI time by pre-checking locally
  • Better integration with editors/IDEs
  • Flexible workflow support

Metadata

Metadata

Assignees

Labels

choreLinting, formatting, dependency hygiene, or project maintenance chorescicdIssue with CI/CD process (GitHub Actions, scaffolding)devopsDevOps activities (containers, automation, deployment, makefiles, etc)triageIssues / Features awaiting triage

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions