From 44a6faad96406dd08d9268921f608e94fcfc7df9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 18:46:38 +0000 Subject: [PATCH 1/8] feat: add Makefile validator Add a new Makefile validator tool with the following features: - Real-time syntax validation - Detection of common Makefile errors (tabs vs spaces in recipes) - Target and dependency parsing - Two view modes: Issues view and Structure view - Line numbers display - Sample Makefile loader - Copy and download functionality - Validation of targets, dependencies, variables, and recipes The validator follows the established pattern used by other validators in the project (JSON, YAML) and integrates seamlessly into the app. --- src/App.tsx | 9 + src/components/MakefileValidator.tsx | 499 +++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 src/components/MakefileValidator.tsx diff --git a/src/App.tsx b/src/App.tsx index 36dd698..d4f0abc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -65,6 +65,7 @@ import HtmlPreview from "./components/HtmlPreview"; import TextDiff from "./components/TextDiff"; import YamlToJson from "./components/YamlToJson"; import YamlFormatter from "./components/YamlFormatter"; +import MakefileValidator from "./components/MakefileValidator"; import NumberBaseConverter from "./components/NumberBaseConverter"; import LoremIpsum from "./components/LoremIpsum"; import JsonToCsv from "./components/JsonToCsv"; @@ -534,6 +535,14 @@ function AppContent() { url: "/yaml-formatter", isEnabled: true, }, + { + id: "makefile-validator", + name: "Makefile Validator", + icon: , + component: , + url: "/makefile-validator", + isEnabled: true, + }, { id: "json-validator", name: "JSON Validator", diff --git a/src/components/MakefileValidator.tsx b/src/components/MakefileValidator.tsx new file mode 100644 index 0000000..711abfa --- /dev/null +++ b/src/components/MakefileValidator.tsx @@ -0,0 +1,499 @@ +import { useState, useEffect } from "react"; +import { Copy, Download, CheckCircle, XCircle, AlertCircle } from "lucide-react"; + +interface ValidationIssue { + line: number; + type: "error" | "warning"; + message: string; +} + +interface MakefileRule { + target: string; + dependencies: string[]; + recipes: string[]; + lineNumber: number; +} + +export default function MakefileValidator() { + const [input, setInput] = useState(""); + const [issues, setIssues] = useState([]); + const [rules, setRules] = useState([]); + const [viewMode, setViewMode] = useState<"issues" | "structure">("issues"); + const [showLineNumbers, setShowLineNumbers] = useState(true); + const [isValid, setIsValid] = useState(null); + + const validateMakefile = () => { + if (!input.trim()) { + setIssues([]); + setRules([]); + setIsValid(null); + return; + } + + const lines = input.split("\n"); + const foundIssues: ValidationIssue[] = []; + const foundRules: MakefileRule[] = []; + let currentRule: MakefileRule | null = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineNum = i + 1; + + // Skip empty lines and comments + if (!line.trim() || line.trim().startsWith("#")) { + if (currentRule) { + foundRules.push(currentRule); + currentRule = null; + } + continue; + } + + // Check for target definition (line with colon not in a recipe) + if (line.match(/^[^\t].*:/) && !line.startsWith("\t")) { + // Save previous rule + if (currentRule) { + foundRules.push(currentRule); + } + + // Parse target and dependencies + const colonIndex = line.indexOf(":"); + const target = line.substring(0, colonIndex).trim(); + const depsStr = line.substring(colonIndex + 1).trim(); + const dependencies = depsStr ? depsStr.split(/\s+/) : []; + + // Validate target name + if (!target) { + foundIssues.push({ + line: lineNum, + type: "error", + message: "Empty target name", + }); + } else if (target.includes(" ") && !target.includes("%")) { + foundIssues.push({ + line: lineNum, + type: "error", + message: "Target name contains spaces (should be separated into multiple targets or use escaping)", + }); + } + + currentRule = { + target, + dependencies, + recipes: [], + lineNumber: lineNum, + }; + } + // Check for recipe lines (must start with tab) + else if (line.startsWith("\t")) { + if (!currentRule) { + foundIssues.push({ + line: lineNum, + type: "error", + message: "Recipe line without a target", + }); + } else { + currentRule.recipes.push(line.substring(1)); + } + } + // Check for lines that should be recipes but use spaces + else if (line.match(/^[ ]{2,}/)) { + foundIssues.push({ + line: lineNum, + type: "error", + message: "Recipe line starts with spaces instead of a tab (Makefiles require tabs for recipes)", + }); + } + // Variable assignment + else if (line.match(/^[A-Za-z_][A-Za-z0-9_]*\s*[:?+]?=/)) { + // Valid variable assignment + if (currentRule) { + foundRules.push(currentRule); + currentRule = null; + } + + // Check for common variable issues + if (line.includes("=") && !line.match(/[:?+]?=/)) { + foundIssues.push({ + line: lineNum, + type: "warning", + message: "Unusual variable assignment syntax", + }); + } + } + // .PHONY and other special targets + else if (line.match(/^\.[A-Z_]+:/)) { + // Special target like .PHONY, .SILENT, etc. + if (currentRule) { + foundRules.push(currentRule); + currentRule = null; + } + } + // Include directives + else if (line.match(/^-?include\s+/)) { + if (currentRule) { + foundRules.push(currentRule); + currentRule = null; + } + } + // Conditional directives + else if (line.match(/^(ifdef|ifndef|ifeq|ifneq|else|endif)\b/)) { + // Valid conditional directive + if (currentRule) { + foundRules.push(currentRule); + currentRule = null; + } + } + // Line continuation + else if (line.trim().endsWith("\\")) { + // Line continuation is valid, continue to next line + continue; + } + // Unrecognized line format + else if (line.trim()) { + foundIssues.push({ + line: lineNum, + type: "warning", + message: "Unrecognized line format - may be a syntax error or continuation", + }); + } + } + + // Save last rule + if (currentRule) { + foundRules.push(currentRule); + } + + // Check for common Makefile issues + for (const rule of foundRules) { + if (rule.recipes.length === 0 && !rule.target.startsWith(".")) { + foundIssues.push({ + line: rule.lineNumber, + type: "warning", + message: `Target "${rule.target}" has no recipes`, + }); + } + } + + setIssues(foundIssues); + setRules(foundRules); + setIsValid(foundIssues.filter((i) => i.type === "error").length === 0); + }; + + useEffect(() => { + const timeoutId = setTimeout(() => { + validateMakefile(); + }, 300); + + return () => clearTimeout(timeoutId); + }, [input]); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(input); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + const handleDownload = () => { + const blob = new Blob([input], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "Makefile"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const loadSample = () => { + const sample = `# Sample Makefile +CC = gcc +CFLAGS = -Wall -Wextra -O2 +TARGET = myprogram +SOURCES = main.c utils.c helper.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean install + +all: $(TARGET) + +$(TARGET): $(OBJECTS) +\t$(CC) $(CFLAGS) -o $@ $^ + +%.o: %.c +\t$(CC) $(CFLAGS) -c $< -o $@ + +clean: +\trm -f $(OBJECTS) $(TARGET) + +install: $(TARGET) +\tcp $(TARGET) /usr/local/bin/ + +test: $(TARGET) +\t./$(TARGET) --test + +run: $(TARGET) +\t./$(TARGET)`; + setInput(sample); + }; + + const renderLineNumbers = () => { + const lineCount = input.split("\n").length; + return ( +
+ {Array.from({ length: lineCount }, (_, i) => ( +
+ {i + 1} +
+ ))} +
+ ); + }; + + return ( +
+
+

Makefile Validator

+

+ Validate Makefile syntax, check for common errors, and analyze structure +

+
+ +
+ {/* Input Section */} +
+
+

Input

+
+ + + +
+
+ +
+ {showLineNumbers && renderLineNumbers()} +