diff --git a/internal/analysis/diagnostics.go b/internal/analysis/diagnostics.go index 1959c33d..bb78d147 100644 --- a/internal/analysis/diagnostics.go +++ b/internal/analysis/diagnostics.go @@ -50,9 +50,18 @@ func checkSpacesInRecipes(mf *model.Makefile) []lsp.Diagnostic { // checkUndefinedTargetDeps warns about deps that reference undefined targets. func checkUndefinedTargetDeps(mf *model.Makefile) []lsp.Diagnostic { - targetSet := make(map[string]bool, len(mf.Targets)) - for _, t := range mf.Targets { - targetSet[t.Name] = true + foundTarget := func(s string) bool { + for _, t := range mf.Targets { + if t.Name == s { + return true + } + prefix, suffix, hasPattern := strings.Cut(t.Name, "%") + if hasPattern && len(s) > len(prefix)+len(suffix) && + strings.HasPrefix(s, prefix) && strings.HasSuffix(s, suffix) { + return true + } + } + return false } var diags []lsp.Diagnostic @@ -61,7 +70,7 @@ func checkUndefinedTargetDeps(mf *model.Makefile) []lsp.Diagnostic { if shouldSkipDepCheck(dep.Name) { continue } - if !targetSet[dep.Name] { + if !foundTarget(dep.Name) { sev := lsp.SeverityWarning diags = append(diags, lsp.Diagnostic{ Range: dep.Range, diff --git a/internal/analysis/diagnostics_test.go b/internal/analysis/diagnostics_test.go index 8035fa3a..b6415f28 100644 --- a/internal/analysis/diagnostics_test.go +++ b/internal/analysis/diagnostics_test.go @@ -63,6 +63,40 @@ func TestDiagnoseSkipsPatternDeps(t *testing.T) { } } +func TestDiagnoseFindsPatternDeps(t *testing.T) { + input := `all: src/main +src/%: dest/% + $(CC) -o app $^ +` + mf := parser.Parse(testURI, input) + diags := Diagnose(mf) + + for _, d := range diags { + if d.Severity != nil && *d.Severity == lsp.SeverityWarning { + assert.NotContains(t, d.Message, "undefined target") + } + } +} + +func TestPatternRuleDoesNotSuppressUnrelatedWarnings(t *testing.T) { + input := `all: nonexistent + echo hi + +%.o: %.c + $(CC) -c $< -o $@ +` + mf := parser.Parse(testURI, input) + diags := Diagnose(mf) + + var found bool + for _, d := range diags { + if d.Message == "undefined target: nonexistent" { + found = true + } + } + assert.True(t, found) +} + func TestDiagnoseSkipsFileDeps(t *testing.T) { input := `all: main.o utils.o $(CC) -o app $^ diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 9b6854ef..a8671df3 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -198,6 +198,18 @@ func TestParseStaticPatternRule(t *testing.T) { assert.Equal(t, "%.c", tgt.PrereqPattern) } +func TestParsePrefixedPatternRule(t *testing.T) { + input := `src/%: dest/% + $(CC) -c $< -o $@ +` + m := Parse(testURI, input) + + require.Len(t, m.Targets, 1) + tgt := m.Targets[0] + assert.True(t, tgt.IsPattern) + assert.Equal(t, "src/%", tgt.Name) +} + func TestParseOrderOnlyDeps(t *testing.T) { input := `build: main.o utils.o | builddir $(CC) -o $@ $^