pkgsrc/pkgtools/pkglint/files/autofix_test.go
rillig f4f2d9ce42 pkgtools/pkglint: update to 21.4.1
Changes since 21.4.0:

Running 'pkglint doc/CHANGES-2021' now warns about issues for this
single file.  Previously, it was necessary to specify '-Cglobal' as
well, but then pkglint also warned about issues in all other CHANGES
files.

Pkglint no longer warns about the characters '!' and '@' in
GO_MODULES_FILES, since these are legitimate.  Fixes PR pkg/56595.

Small cleanups in the pkglint testing infrastructure.
2022-01-01 12:44:24 +00:00

1484 lines
40 KiB
Go

package pkglint
import (
"gopkg.in/check.v1"
"netbsd.org/pkglint/regex"
"os"
"runtime"
"strings"
)
func (s *Suite) Test_Autofix__default_also_updates_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--source")
mklines := t.SetUpFileMkLines("Makefile",
"# row 1 \\",
"continuation of row 1")
line := mklines.lines.Lines[0]
fix := line.Autofix()
fix.Warnf("Row should be replaced with line.")
fix.Replace("row", "line")
fix.InsertAbove("above")
fix.InsertBelow("below")
fix.Delete()
fix.Apply()
t.CheckEquals(fix.RawText(), ""+
"above\n"+
"below\n")
t.CheckOutputLines(
">\t# row 1 \\",
">\tcontinuation of row 1",
"WARN: ~/Makefile:1--2: Row should be replaced with line.")
t.CheckEquals(fix.modified, true)
}
func (s *Suite) Test_Autofix__show_autofix_modifies_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--source", "--show-autofix")
mklines := t.SetUpFileMkLines("Makefile",
"# row 1 \\",
"continuation of row 1")
line := mklines.lines.Lines[0]
fix := line.Autofix()
fix.Warnf("Row should be replaced with line.")
fix.ReplaceAfter("", "# row", "# line")
fix.InsertAbove("above")
fix.InsertBelow("below")
fix.Delete()
fix.Apply()
t.CheckEquals(fix.RawText(), ""+
"above\n"+
"below\n")
t.CheckOutputLines(
"WARN: ~/Makefile:1--2: Row should be replaced with line.",
"AUTOFIX: ~/Makefile:1: Replacing \"# row\" with \"# line\".",
"AUTOFIX: ~/Makefile:1: Inserting a line \"above\" above this line.",
"AUTOFIX: ~/Makefile:2: Inserting a line \"below\" below this line.",
"AUTOFIX: ~/Makefile:1: Deleting this line.",
"AUTOFIX: ~/Makefile:2: Deleting this line.",
"+\tabove",
"-\t# row 1 \\",
"-\tcontinuation of row 1",
"+\tbelow")
t.CheckEquals(fix.modified, true)
}
func (s *Suite) Test_Autofix__multiple_fixes(c *check.C) {
t := s.Init(c)
newRawLines := func(texts ...string) []*RawLine {
var rawLines []*RawLine
for _, text := range texts {
rawLines = append(rawLines, &RawLine{text})
}
return rawLines
}
t.SetUpCommandLine("--show-autofix", "--explain")
line := t.NewLine("filename", 1, "original")
t.CheckNil(line.fix)
t.CheckDeepEquals(line.raw, newRawLines("original\n"))
{
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
fix.Replace("original", "lriginao")
fix.Apply()
}
t.CheckDeepEquals(line.raw, newRawLines("original\n"))
t.CheckDeepEquals(line.fix.texts, []string{"lriginao\n"})
t.CheckOutputLines(
"AUTOFIX: filename:1: Replacing \"original\" with \"lriginao\".")
{
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
fix.Replace("ig", "ug")
fix.Apply()
}
t.CheckDeepEquals(line.raw, newRawLines("original\n"))
t.CheckDeepEquals(line.fix.texts, []string{"lruginao\n"})
t.CheckEquals(line.RawText(0), "lruginao")
t.CheckOutputLines(
"AUTOFIX: filename:1: Replacing \"ig\" with \"ug\".")
{
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
fix.Replace("lruginao", "middle")
fix.Apply()
}
t.CheckDeepEquals(line.raw, newRawLines("original\n"))
t.CheckDeepEquals(line.fix.texts, []string{"middle\n"})
t.CheckOutputLines(
"AUTOFIX: filename:1: Replacing \"lruginao\" with \"middle\".")
t.CheckDeepEquals(line.fix.texts, []string{"middle\n"})
t.CheckOutputEmpty()
{
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
fix.Delete()
fix.Apply()
}
t.CheckEquals(line.Autofix().RawText(), "")
t.CheckOutputLines(
"AUTOFIX: filename:1: Deleting this line.")
}
// Up to 2018-11-25, pkglint in some cases logged only the source without
// a corresponding warning.
func (s *Suite) Test_Autofix__lonely_source(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("-Wall", "--source")
G.Logger.verbose = false // For realistic conditions; otherwise all diagnostics are logged.
t.SetUpPackage("x11/xorg-cf-files",
".include \"../../x11/xorgproto/buildlink3.mk\"")
t.SetUpPackage("x11/xorgproto",
"DISTNAME=\txorgproto-1.0")
t.CreateFileBuildlink3("x11/xorgproto/buildlink3.mk")
t.CreateFileLines("x11/xorgproto/builtin.mk",
MkCvsID,
"",
"BUILTIN_PKG:=\txorgproto",
"",
"PRE_XORGPROTO_LIST_MISSING =\tapplewmproto",
"",
".for id in ${PRE_XORGPROTO_LIST_MISSING}",
".endfor")
t.Chdir(".")
t.FinishSetUp()
G.Check("x11/xorg-cf-files")
G.Check("x11/xorgproto")
t.CheckOutputLines(
">\tPRE_XORGPROTO_LIST_MISSING =\tapplewmproto",
"NOTE: x11/xorg-cf-files/../../x11/xorgproto/builtin.mk:5: "+
"Unnecessary space after variable name \"PRE_XORGPROTO_LIST_MISSING\".")
}
// Up to 2018-11-26, pkglint in some cases logged only the source without
// a corresponding warning.
func (s *Suite) Test_Autofix__lonely_source_2(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("-Wall", "--source", "--explain")
G.Logger.verbose = false // For realistic conditions; otherwise all diagnostics are logged.
t.SetUpPackage("print/tex-bibtex8",
"# Including bsd.prefs.mk is not necessary here since",
"# PKGSRC_COMPILER is evaluated lazily.",
"",
"MAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}")
t.Chdir(".")
t.FinishSetUp()
G.Check("print/tex-bibtex8")
t.CheckOutputLines(
">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
"WARN: print/tex-bibtex8/Makefile:23: Please use ${CFLAGS.${PKGSRC_COMPILER}:Q} instead of ${CFLAGS.${PKGSRC_COMPILER}}.",
"",
"\tSee the pkgsrc guide, section \"Echoing a string exactly as-is\":",
"\thttps://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#echo-literal",
"",
">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
"WARN: print/tex-bibtex8/Makefile:23: The list variable PKGSRC_COMPILER should not be embedded in a word.",
"",
"\tWhen a list variable has multiple elements, this expression expands",
"\tto something unexpected:",
"",
"\tExample: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
"",
"\t\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/",
"",
"\tThe first URL is missing the directory. To fix this, write",
"\t\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
"",
"\tExample: -l${LIBS} expands to",
"",
"\t\t-llib1 lib2",
"",
"\tThe second library is missing the -l. To fix this, write",
"\t${LIBS:S,^,-l,}.",
"")
}
func (s *Suite) Test_Autofix__show_autofix_and_source_continuation_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
mklines := t.SetUpFileMkLines("Makefile",
MkCvsID,
"# before \\",
"The old song \\",
"after")
line := mklines.lines.Lines[1]
fix := line.Autofix()
fix.Warnf("Using \"old\" is deprecated.")
fix.Replace("old", "new")
fix.Apply()
// Using a tab for indentation preserves the exact layout in the output
// since in pkgsrc Makefiles, tabs are also used in the middle of the line
// to align the variable values. Using a single space for indentation would
// make some of the lines appear misaligned in the pkglint output although
// they are correct in the Makefiles.
t.CheckOutputLines(
"WARN: ~/Makefile:3: Using \"old\" is deprecated.",
"AUTOFIX: ~/Makefile:3: Replacing \"old\" with \"new\".",
"\t# before \\",
"-\tThe old song \\",
"+\tThe new song \\",
"\tafter")
}
// Demonstrates that without the --show-autofix option, diagnostics are
// shown even when they cannot be autofixed.
//
// This is typical when an autofix is provided for simple scenarios,
// but the code actually found is a little more complicated, like needing
// special escaping for some of the characters or containing linebreaks.
func (s *Suite) Test_Autofix__show_unfixable_diagnostics_in_default_mode(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--source")
lines := t.NewLines("Makefile",
"line1",
"line2",
"line3")
lines.Lines[0].Warnf("This warning is shown since the --show-autofix option is not given.")
fix := lines.Lines[1].Autofix()
fix.Warnf("This warning cannot be fixed, nevertheless it is shown (1).")
fix.Replace("XXX", "TODO")
fix.Apply()
fix.Warnf("This warning cannot be fixed, nevertheless it is shown (2).")
fix.Replace("XXX", "TODO")
fix.Apply()
lines.Lines[2].Warnf("This warning is also shown.")
t.CheckOutputLines(
">\tline1",
"WARN: Makefile:1: This warning is shown since the --show-autofix option is not given.",
"",
">\tline2",
"WARN: Makefile:2: This warning cannot be fixed, nevertheless it is shown (1).",
"WARN: Makefile:2: This warning cannot be fixed, nevertheless it is shown (2).",
"",
">\tline3",
"WARN: Makefile:3: This warning is also shown.")
}
// Demonstrates that the --show-autofix option only shows those diagnostics
// that would be fixed.
func (s *Suite) Test_Autofix__suppress_unfixable_warnings_with_show_autofix(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
lines := t.NewLines("Makefile",
"line1",
"line2",
"line3")
lines.Lines[0].Warnf("This warning is not shown since it is not part of a fix.")
fix := lines.Lines[1].Autofix()
fix.Warnf("Something's wrong here.")
fix.ReplaceAt(0, 0, "line2", "XXX")
fix.Apply()
fix.Warnf("Since XXX marks are usually not fixed, use TODO instead to draw attention.")
fix.Replace("XXX", "TODO")
fix.Apply()
lines.Lines[2].Warnf("Neither is this warning shown.")
t.CheckOutputLines(
"WARN: Makefile:2: Something's wrong here.",
"AUTOFIX: Makefile:2: Replacing \"line2\" with \"XXX\".",
"-\tline2",
"+\tXXX",
"",
"WARN: Makefile:2: Since XXX marks are usually not fixed, use TODO instead to draw attention.",
"AUTOFIX: Makefile:2: Replacing \"XXX\" with \"TODO\".",
"-\tline2",
"+\tTODO")
}
// If an Autofix doesn't do anything, it nevertheless logs the diagnostics.
func (s *Suite) Test_Autofix__noop_replace(c *check.C) {
t := s.Init(c)
line := t.NewLine("Makefile", 14, "Original text")
fix := line.Autofix()
fix.Warnf("The word ABC should not be used.")
fix.Replace("ABC", "---censored---")
fix.Apply()
// This warning is wrong since the actual line doesn't contain the
// word ABC.
//
// This test therefore demonstrates that each autofix must be properly
// guarded to only apply when it actually does something.
//
// As of November 2019 there is no Rollback method since it was not
// needed yet.
t.CheckOutputLines(
"WARN: Makefile:14: The word ABC should not be used.")
}
// Contrary to Line.Autofix(), the NewAutofix constructor does not check
// whether the previous autofix is already finished, since it cannot know.
func (s *Suite) Test_NewAutofix(c *check.C) {
t := s.Init(c)
line := t.NewLine("filename.mk", 123, "")
fix := NewAutofix(line)
fix2 := NewAutofix(line)
t.CheckEquals(fix2 == fix, false)
t.CheckDeepEquals(fix2, fix)
}
func (s *Suite) Test_Autofix_Errorf(c *check.C) {
t := s.Init(c)
line := t.NewLine("DESCR", 1, "Description of the package")
fix := line.Autofix()
fix.Errorf("Error.")
fix.Apply()
t.CheckOutputLines(
"ERROR: DESCR:1: Error.")
}
func (s *Suite) Test_Autofix_Warnf__duplicate(c *check.C) {
t := s.Init(c)
line := t.NewLine("DESCR", 1, "Description of the package")
fix := line.Autofix()
fix.Warnf("Warning 1.")
t.ExpectAssert(func() { fix.Warnf("Warning 2.") })
}
func (s *Suite) Test_Autofix_Notef(c *check.C) {
t := s.Init(c)
line := t.NewLine("DESCR", 1, "Description of the package")
fix := line.Autofix()
fix.Notef("Note.")
fix.Apply()
t.CheckOutputLines(
"NOTE: DESCR:1: Note.")
}
func (s *Suite) Test_Autofix_Silent(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix")
line := t.NewLine("DESCR", 1, "Description of the package")
fix := line.Autofix()
fix.Silent()
fix.Replace("package", "replaced package")
fix.Apply()
t.CheckOutputLines(
"AUTOFIX: DESCR:1: Replacing \"package\" with \"replaced package\".")
t.CheckEquals(line.fix.texts[0], "Description of the replaced package\n")
}
func (s *Suite) Test_Autofix_Explain__without_explain_option(c *check.C) {
t := s.Init(c)
line := t.NewLine("Makefile", 74, "line1")
fix := line.Autofix()
fix.Warnf("Please write row instead of line.")
fix.Replace("line", "row")
fix.Explain("Explanation")
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:74: Please write row instead of line.")
t.CheckEquals(G.Logger.explanationsAvailable, true)
}
func (s *Suite) Test_Autofix_Explain__default(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--explain")
line := t.NewLine("Makefile", 74, "line1")
fix := line.Autofix()
fix.Warnf("Please write row instead of line.")
fix.Replace("line", "row")
fix.Explain("Explanation")
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:74: Please write row instead of line.",
"",
"\tExplanation",
"")
t.CheckEquals(G.Logger.explanationsAvailable, true)
}
func (s *Suite) Test_Autofix_Explain__show_autofix(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--explain")
line := t.NewLine("Makefile", 74, "line1")
fix := line.Autofix()
fix.Warnf("Please write row instead of line.")
fix.Replace("line", "row")
fix.Explain("Explanation")
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:74: Please write row instead of line.",
"AUTOFIX: Makefile:74: Replacing \"line\" with \"row\".",
"",
"\tExplanation",
"")
t.CheckEquals(G.Logger.explanationsAvailable, true)
}
func (s *Suite) Test_Autofix_Explain__autofix(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--explain")
line := t.NewLine("Makefile", 74, "line1")
fix := line.Autofix()
fix.Warnf("Please write row instead of line.")
fix.Replace("line", "row")
fix.Explain("Explanation")
fix.Apply()
t.CheckOutputLines(
"AUTOFIX: Makefile:74: Replacing \"line\" with \"row\".")
t.CheckEquals(G.Logger.explanationsAvailable, false) // Not necessary.
}
func (s *Suite) Test_Autofix_Explain__SilentAutofixFormat(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--explain")
line := t.NewLine("example.txt", 1, "Text")
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
t.ExpectAssert(func() { fix.Explain("Explanation for inserting a line before.") })
}
// To combine a silent diagnostic with an explanation, two separate autofixes
// are necessary.
func (s *Suite) Test_Autofix_Explain__silent_with_diagnostic(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--explain")
line := t.NewLine("example.txt", 1, "Text")
fix := line.Autofix()
fix.Warnf(SilentAutofixFormat)
fix.InsertAbove("above")
fix.Apply()
fix.Notef("This diagnostic is necessary for the following explanation.")
fix.Explain(
"When inserting multiple lines, Apply must be called in-between.",
"Otherwise the changes are not described to the human reader.")
fix.InsertBelow("below")
fix.Apply()
t.CheckOutputLines(
"NOTE: example.txt:1: This diagnostic is necessary for the following explanation.",
"",
"\tWhen inserting multiple lines, Apply must be called in-between.",
"\tOtherwise the changes are not described to the human reader.",
"")
t.CheckEquals(fix.RawText(), "above\nText\nbelow\n")
}
func (s *Suite) Test_Autofix_Replace(c *check.C) {
t := s.Init(c)
doTest := func(bool) {
line := t.NewLine("filename.mk", 123, "text")
fix := line.Autofix()
fix.Warnf("Warning.")
fix.Replace("text", "replacement")
fix.Apply()
}
t.ExpectDiagnosticsAutofix(
doTest,
"WARN: filename.mk:123: Warning.",
"AUTOFIX: filename.mk:123: Replacing \"text\" with \"replacement\".")
}
func (s *Suite) Test_Autofix_ReplaceAfter__autofix_in_continuation_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--source")
mklines := t.SetUpFileMkLines("Makefile",
"# line 1 \\",
"continuation 1 \\",
"continuation 2")
fix := mklines.lines.Lines[0].Autofix()
fix.Warnf("Line should be replaced with Row.")
fix.ReplaceAfter("", "line", "row")
fix.Apply()
t.CheckOutputLines(
"AUTOFIX: ~/Makefile:1: Replacing \"line\" with \"row\".",
"-\t# line 1 \\",
"+\t# row 1 \\",
"\tcontinuation 1 \\",
"\tcontinuation 2")
}
func (s *Suite) Test_Autofix_ReplaceAfter__autofix_several_times_in_continuation_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--source")
mklines := t.SetUpFileMkLines("Makefile",
"# line 1 \\",
"continuation 1 \\",
"continuation 2")
fix := mklines.lines.Lines[0].Autofix()
fix.Warnf("N should be replaced with V.")
fix.ReplaceAfter("", "n", "v")
fix.Apply()
// Nothing is logged or fixed because the "n" appears more than once,
// and as of June 2019, pkglint doesn't know which occurrence is the
// correct one.
t.CheckOutputEmpty()
}
func (s *Suite) Test_Autofix_ReplaceAfter__autofix_one_time(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--source")
mklines := t.SetUpFileMkLines("Makefile",
MkCvsID,
"VAR=\t$$(var) $(var)")
mklines.Check()
// Nothing is replaced since, as of June 2019, pkglint doesn't
// know which of the two "$(var)" should be replaced.
t.CheckOutputEmpty()
}
// When an autofix replaces text, it does not touch those
// lines that have been inserted before since these are
// usually already correct.
func (s *Suite) Test_Autofix_ReplaceAfter__after_inserting_a_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix")
line := t.NewLine("filename", 5, "initial text")
fix := line.Autofix()
fix.Notef("Inserting a line.")
fix.InsertAbove("line above")
fix.InsertBelow("line below")
fix.Apply()
fix.Notef("Replacing text.")
fix.Replace("l", "L")
fix.ReplaceAt(0, 0, "i", "I")
fix.Apply()
t.CheckOutputLines(
"NOTE: filename:5: Inserting a line.",
"AUTOFIX: filename:5: Inserting a line \"line above\" above this line.",
"AUTOFIX: filename:5: Inserting a line \"line below\" below this line.",
"NOTE: filename:5: Replacing text.",
"AUTOFIX: filename:5: Replacing \"l\" with \"L\".",
"AUTOFIX: filename:5: Replacing \"i\" with \"I\".")
}
func (s *Suite) Test_Autofix_ReplaceAfter__replace_once(c *check.C) {
t := s.Init(c)
doTest := func(autofix bool) {
mklines := t.NewMkLines("filename.mk",
"# before ##### after")
mklines.ForEach(func(mkline *MkLine) {
fix := mkline.Autofix()
fix.Warnf("Warning.")
fix.ReplaceAfter("", "###", "replaced")
fix.Apply()
})
}
t.ExpectDiagnosticsAutofix(
doTest,
"WARN: filename.mk:1: Warning.")
// No autofix since it is not clear which of the 3 possible
// ### is meant.
}
func (s *Suite) Test_Autofix_ReplaceAfter__replace_once_escaped(c *check.C) {
t := s.Init(c)
doTest := func(autofix bool) {
G.Logger.Opts.ShowSource = true
mklines := t.NewMkLines("filename.mk",
"VAR=\tvalue \\#\\#\\# # comment ###")
mklines.ForEach(func(mkline *MkLine) {
fix := mkline.Autofix()
fix.Warnf("Warning.")
fix.ReplaceAfter("", "###", "replaced")
fix.Apply()
})
}
// This may be the wrong replacement since the part before the
// comment is already unescaped when most of the checks run,
// and the tests then try to replace the parsed text instead of
// the original text as it appears in the actual file.
//
// This is most probably an edge case. As soon as pkglint parses
// the lines into tokens containing exact positioning information,
// this can be easily fixed as a by-product.
t.ExpectDiagnosticsAutofix(
doTest,
">\tVAR=\tvalue \\#\\#\\# # comment ###",
"WARN: filename.mk:1: Warning.",
"AUTOFIX: filename.mk:1: Replacing \"###\" with \"replaced\".",
"-\tVAR=\tvalue \\#\\#\\# # comment ###",
"+\tVAR=\tvalue \\#\\#\\# # comment replaced")
}
func (s *Suite) Test_Autofix_ReplaceAt(c *check.C) {
t := s.Init(c)
mainPart := func(texts []string, rawIndex int, column int, from, to string) {
mklines := t.NewMkLines("filename.mk", texts...)
assert(len(mklines.mklines) == 1)
mkline := mklines.mklines[0]
fix := mkline.Autofix()
fix.Notef("Should be appended instead of assigned.")
fix.ReplaceAt(rawIndex, column, from, to)
fix.Apply()
}
lines := func(lines ...string) []string { return lines }
test := func(texts []string, rawIndex int, column int, from, to string, diagnostics ...string) {
doTest := func(bool) {
mainPart(texts, rawIndex, column, from, to)
}
t.ExpectDiagnosticsAutofix(doTest, diagnostics...)
}
testAssert := func(texts []string, rawIndex int, column int, from, to string) {
doTest := func(bool) {
t.ExpectAssert(
func() { mainPart(texts, rawIndex, column, from, to) })
}
t.ExpectDiagnosticsAutofix(doTest, nil...)
}
testPanicMatches := func(texts []string, rawIndex int, column int, from, to string, panicMessage regex.Pattern) {
doTest := func(bool) {
t.ExpectPanicMatches(
func() { mainPart(texts, rawIndex, column, from, to) },
panicMessage)
}
t.ExpectDiagnosticsAutofix(doTest, nil...)
}
test(
lines(
"VAR=value1 \\",
"\tvalue2"),
0, 3, "=", "+=",
"NOTE: filename.mk:1: Should be appended instead of assigned.",
"AUTOFIX: filename.mk:1: Replacing \"=\" with \"+=\".")
// If the text at the precisely given position does not match,
// it is a programming mistake, therefore pkglint panics.
testAssert(
lines(
"VAR=value1 \\",
"\tvalue2"),
0, 3, "?", "+=")
// Getting the line number wrong is a strange programming error, and
// there does not need to be any code checking for this in the main code.
testPanicMatches(
lines(
"VAR=value"),
10, 3, "from", "to",
`runtime error: index out of range.*`)
// It is a programming error to replace a string with itself, since that
// would produce confusing diagnostics.
testAssert(
lines(
"VAR=value"),
0, 4, "value", "value")
// Getting the column number wrong may happen when a previous replacement
// has made the string shorter than before.
// This is a programming mistake, therefore panic.
// All fixes that work on the raw lines are supposed to work exactly and
// know what they are doing.
testAssert(
lines(
"VAR=value1 \\",
"\tvalue2"),
0, 20, "?", "+=")
}
func (s *Suite) Test_Autofix_ReplaceAt__only(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--only", "specific", "--autofix")
mklines := t.SetUpFileMkLines("filename.mk",
"# comment")
mklines.ForEach(func(mkline *MkLine) {
// The modifications from this replacement are not saved to the file.
// They are only applied to the in-memory copy.
fix := mkline.Autofix()
fix.Warnf("Warning.")
fix.ReplaceAt(0, 0, "# ", "COMMENT=\t")
fix.Apply()
// This autofix marks the file's lines as changed.
// Without it, SaveAutofixChanges would not have any effect.
fix = mkline.Autofix()
fix.Warnf("A specific warning.")
fix.Replace("comment", "remark")
fix.Apply()
})
mklines.SaveAutofixChanges()
t.CheckOutputLines(
"AUTOFIX: ~/filename.mk:1: Replacing \"comment\" with \"remark\".")
t.CheckFileLines("filename.mk",
"# remark")
}
func (s *Suite) Test_Autofix_InsertAbove(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
line := t.NewLine("Makefile", 30, "original")
fix := line.Autofix()
fix.Warnf("Dummy.")
fix.InsertAbove("inserted")
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:30: Dummy.",
"AUTOFIX: Makefile:30: Inserting a line \"inserted\" above this line.",
"+\tinserted",
">\toriginal")
}
func (s *Suite) Test_Autofix_InsertBelow(c *check.C) {
t := s.Init(c)
doTest := func(bool) {
line := t.NewLine("DESCR", 1, "Description of the package")
fix := line.Autofix()
fix.Errorf("Error.")
fix.InsertBelow("below")
fix.Apply()
}
t.ExpectDiagnosticsAutofix(
doTest,
"ERROR: DESCR:1: Error.",
"AUTOFIX: DESCR:1: Inserting a line \"below\" below this line.")
}
func (s *Suite) Test_Autofix_Delete(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
line := t.NewLine("Makefile", 30, "to be deleted")
fix := line.Autofix()
fix.Warnf("Dummy.")
fix.Delete()
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:30: Dummy.",
"AUTOFIX: Makefile:30: Deleting this line.",
"-\tto be deleted")
}
// Deleting a line from a Makefile also deletes its continuation lines.
func (s *Suite) Test_Autofix_Delete__continuation_line(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
mklines := t.SetUpFileMkLines("Makefile",
MkCvsID,
"# line 1 \\",
"continued")
line := mklines.lines.Lines[1]
fix := line.Autofix()
fix.Warnf("Dummy warning.")
fix.Delete()
fix.Apply()
t.CheckOutputLines(
"WARN: ~/Makefile:2--3: Dummy warning.",
"AUTOFIX: ~/Makefile:2: Deleting this line.",
"AUTOFIX: ~/Makefile:3: Deleting this line.",
"-\t# line 1 \\",
"-\tcontinued")
}
func (s *Suite) Test_Autofix_Delete__combined_with_insert(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix", "--source")
line := t.NewLine("Makefile", 30, "to be deleted")
fix := line.Autofix()
fix.Warnf("This line should be replaced completely.")
fix.Delete()
fix.InsertBelow("below")
fix.InsertAbove("above")
fix.Apply()
t.CheckOutputLines(
"WARN: Makefile:30: This line should be replaced completely.",
"AUTOFIX: Makefile:30: Deleting this line.",
"AUTOFIX: Makefile:30: Inserting a line \"below\" below this line.",
"AUTOFIX: Makefile:30: Inserting a line \"above\" above this line.",
"+\tabove",
"-\tto be deleted",
"+\tbelow")
}
// When using Autofix.Custom, it is tricky to get all the details right.
// For best results, see the existing examples and the documentation.
//
// Since this custom fix only operates on the text of the current line,
// it can handle both the --show-autofix and the --autofix cases using
// the same code.
func (s *Suite) Test_Autofix_Custom__in_memory(c *check.C) {
t := s.Init(c)
lines := t.NewLines("Makefile",
"line1",
"line2",
"line3")
doFix := func(line *Line) {
fix := line.Autofix()
fix.Warnf("Please write in ALL-UPPERCASE.")
fix.Custom(func(showAutofix, autofix bool) {
fix.Describef(0, "Converting to uppercase")
if showAutofix || autofix {
line.Text = strings.ToUpper(line.Text)
}
})
fix.Apply()
}
doFix(lines.Lines[0])
t.CheckOutputLines(
"WARN: Makefile:1: Please write in ALL-UPPERCASE.")
t.SetUpCommandLine("--show-autofix")
doFix(lines.Lines[1])
t.CheckOutputLines(
"WARN: Makefile:2: Please write in ALL-UPPERCASE.",
"AUTOFIX: Makefile:2: Converting to uppercase")
t.CheckEquals(lines.Lines[1].Text, "LINE2")
t.SetUpCommandLine("--autofix")
doFix(lines.Lines[2])
t.CheckOutputLines(
"AUTOFIX: Makefile:3: Converting to uppercase")
t.CheckEquals(lines.Lines[2].Text, "LINE3")
}
func (s *Suite) Test_Autofix_Describef(c *check.C) {
t := s.Init(c)
doTest := func(bool) {
line := t.NewLine("DESCR", 123, "Description of the package")
fix := line.Autofix()
fix.Errorf("Error.")
fix.Custom(func(showAutofix, autofix bool) {
fix.Describef(0, "Masking.")
fix.texts[0] = replaceAll(fix.texts[0], `\p{L}`, "*")
})
fix.Apply()
t.CheckEquals(line.RawText(0), "*********** ** *** *******")
}
t.ExpectDiagnosticsAutofix(
doTest,
"ERROR: DESCR:123: Error.",
"AUTOFIX: DESCR:123: Masking.")
}
// With the default command line options, this warning is printed.
// With the --show-autofix option this warning is NOT printed since it
// cannot be fixed automatically.
func (s *Suite) Test_Autofix_Apply__show_autofix_option(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--show-autofix")
line := t.NewLine("filename", 3, "")
fix := line.Autofix()
fix.Warnf("This autofix doesn't match.")
fix.Replace("doesn't match", "")
fix.Apply()
t.CheckOutputEmpty()
t.SetUpCommandLine()
fix.Warnf("This autofix doesn't match.")
fix.Replace("doesn't match", "")
fix.Apply()
t.CheckOutputLines(
"WARN: filename:3: This autofix doesn't match.")
}
func (s *Suite) Test_Autofix_Apply__autofix_option(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
line := t.NewLine("filename", 5, "text")
fix := line.Autofix()
fix.Notef("This line is quite short.")
fix.Replace("not found", "needle")
fix.Apply()
// Because of the --autofix option, the note is not logged.
t.CheckOutputEmpty()
}
func (s *Suite) Test_Autofix_Apply__autofix_and_show_autofix_options_no_match(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--show-autofix")
line := t.NewLine("filename", 5, "text")
fix := line.Autofix()
fix.Notef("This line is quite short.")
fix.Replace("not found", "needle")
fix.Apply()
// Since at least one of the --show-autofix or --autofix options is given,
// only those autofixes that actually change something are logged.
// This one doesn't find the "not found" text, therefore it is not logged.
t.CheckOutputEmpty()
}
// Demonstrates how to filter log messages.
// The --autofix option can restrict the fixes to exactly one group or topic.
func (s *Suite) Test_Autofix_Apply__only(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--source", "--only", "interesting")
line := t.NewLine("Makefile", 27, "The old song")
// Is completely ignored, including any autofixes.
fix := line.Autofix()
fix.Warnf("Using \"old\" is deprecated.")
fix.Replace("old", "new1")
fix.Apply()
fix.Warnf("Using \"old\" is interesting.")
fix.Replace("old", "new2")
fix.Apply()
t.CheckOutputLines(
"AUTOFIX: Makefile:27: Replacing \"old\" with \"new2\".",
"-\tThe old song",
"+\tThe new2 song")
}
func (s *Suite) Test_Autofix_Apply__panic(c *check.C) {
t := s.Init(c)
line := t.NewLine("filename", 123, "text")
t.ExpectAssert(
func() {
fix := line.Autofix()
fix.Apply()
})
t.ExpectAssert(
func() {
fix := line.Autofix()
fix.Replace("from", "to")
fix.Apply()
})
t.ExpectPanic(
func() {
fix := line.Autofix()
fix.Warnf("Warning without period")
fix.Apply()
},
"Pkglint internal error: Autofix: format \"Warning without period\" must end with a period.")
}
// Ensures that empty lines are logged between the diagnostics,
// even when combining normal warnings and autofix warnings.
//
// Up to 2018-10-27, pkglint didn't insert the required empty line in this case.
func (s *Suite) Test_Autofix_Apply__explanation_followed_by_note(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--source")
line := t.NewLine("README.txt", 123, "text")
fix := line.Autofix()
fix.Warnf("A warning with autofix.")
fix.Explain("Explanation.")
fix.Replace("text", "Text")
fix.Apply()
line.Notef("A note without fix.")
t.CheckOutputLines(
">\ttext",
"WARN: README.txt:123: A warning with autofix.",
"NOTE: README.txt:123: A note without fix.")
}
// The --autofix option normally suppresses the diagnostics and just logs
// the actual fixes. Adding the --show-autofix option logs both.
func (s *Suite) Test_Autofix_Apply__autofix_and_show_autofix_options(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--show-autofix")
line := t.NewLine("filename", 5, "text")
fix := line.Autofix()
fix.Notef("This line is quite short.")
fix.Replace("text", "replacement")
fix.Apply()
t.CheckOutputLines(
"NOTE: filename:5: This line is quite short.",
"AUTOFIX: filename:5: Replacing \"text\" with \"replacement\".")
}
// In --autofix mode or --show-autofix mode, those errors that have
// been automatically fixed are not counted, and the others are filtered
// out, therefore the exitcode stays at 0.
func (s *Suite) Test_Autofix_Apply__anyway_error(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
mklines := t.SetUpFileMkLines("filename.mk",
MkCvsID,
"VAR=\tvalue")
fix := mklines.mklines[1].Autofix()
fix.Errorf("From must be To.")
fix.Replace("from", "to")
fix.Apply()
mklines.SaveAutofixChanges()
t.CheckEquals(G.Logger.errors, 0)
t.CheckOutputEmpty()
}
func (s *Suite) Test_Autofix_Apply__source_autofix_no_change(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix", "--source")
lines := t.SetUpFileLines("filename",
"word word word")
fix := lines.Lines[0].Autofix()
fix.Notef("Word should be replaced, but pkglint is not sure which one.")
fix.Replace("word", "replacement")
fix.Apply()
lines.SaveAutofixChanges()
// Nothing is replaced since, as of June 2019, pkglint doesn't
// know which of the three "word" should be replaced.
//
// The note is not logged since --show-autofix nor --autofix is
// given in the command line.
t.CheckOutputEmpty()
t.CheckFileLines("filename",
"word word word")
}
// Ensures that without explanations, the separator between the individual
// diagnostics are generated.
func (s *Suite) Test_Autofix_Apply__source_without_explain(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--source", "--explain", "--show-autofix")
line := t.NewLine("filename", 5, "text")
fix := line.Autofix()
fix.Notef("This line is quite short.")
fix.Replace("text", "replacement")
fix.Apply()
fix.Warnf("Follow-up warning, separated.")
fix.Replace("replacement", "text again")
fix.Apply()
t.CheckOutputLines(
"NOTE: filename:5: This line is quite short.",
"AUTOFIX: filename:5: Replacing \"text\" with \"replacement\".",
"-\ttext",
"+\treplacement",
"",
"WARN: filename:5: Follow-up warning, separated.",
"AUTOFIX: filename:5: Replacing \"replacement\" with \"text again\".",
"-\ttext",
"+\ttext again")
}
// After fixing part of a line, the whole line needs to be parsed again.
//
// As of May 2019, this is not done yet.
func (s *Suite) Test_Autofix_Apply__text_after_replacing(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("-Wall", "--autofix")
mkline := t.NewMkLine("filename.mk", 123, "VAR=\tvalue")
fix := mkline.Autofix()
fix.Notef("Just a demo.")
fix.Replace("value", "new value")
fix.Apply()
t.CheckOutputLines(
"AUTOFIX: filename.mk:123: Replacing \"value\" with \"new value\".")
t.CheckEquals(mkline.fix.texts[0], "VAR=\tnew value\n")
t.CheckEquals(mkline.raw[0].orignl, "VAR=\tvalue\n")
t.CheckEquals(mkline.Text, "VAR=\tnew value")
// TODO: should be updated as well.
t.CheckEquals(mkline.Value(), "value")
}
// Just for branch coverage.
func (s *Suite) Test_Autofix_setDiag__no_testing_mode(c *check.C) {
t := s.Init(c)
line := t.NewLine("file.mk", 123, "text")
G.Testing = false
fix := line.Autofix()
fix.Notef("Note.")
fix.Replace("from", "to")
fix.Apply()
t.CheckOutputLines(
"NOTE: file.mk:123: Note.")
}
func (s *Suite) Test_Autofix_setDiag__bad_call_sequence(c *check.C) {
t := s.Init(c)
line := t.NewLine("file.mk", 123, "text")
fix := line.Autofix()
fix.Notef("Note.")
t.ExpectAssert(func() { fix.Notef("Note 2.") })
fix.level = nil // To cover the second assertion.
t.ExpectAssert(func() { fix.Notef("Note 2.") })
}
// Pkglint tries to order the diagnostics from top to bottom.
// Still, it could be possible that in a multiline the second line
// gets a diagnostic before the first line. This only happens when
// both replacements happen in the same autofix block, which doesn't
// happen often.
//
// This covers the "action.lineno < first" condition.
func (s *Suite) Test_Autofix_affectedLinenos__reverse(c *check.C) {
t := s.Init(c)
test := func(diagnostics ...string) {
mklines := t.NewMkLines("filename.mk",
"VAR=\tline 1 \\",
"\tline 2")
mkline := mklines.mklines[0]
fix := mkline.Autofix()
fix.Warnf("Replacements from bottom to top.")
fix.Replace("line 2", "bbb")
fix.Replace("line 1", "aaa")
fix.Apply()
t.CheckOutput(diagnostics)
}
t.SetUpCommandLine("--source")
test(
">\tVAR=\tline 1 \\",
">\t\tline 2",
"WARN: filename.mk:1--2: Replacements from bottom to top.")
t.SetUpCommandLine("--source", "--show-autofix")
test(
"WARN: filename.mk:1--2: Replacements from bottom to top.",
"AUTOFIX: filename.mk:2: Replacing \"line 2\" with \"bbb\".",
"AUTOFIX: filename.mk:1: Replacing \"line 1\" with \"aaa\".",
"-\tVAR=\tline 1 \\",
"+\tVAR=\taaa \\",
"-\t\tline 2",
"+\t\tbbb")
}
// Since the diagnostic doesn't contain the string "few", nothing happens.
func (s *Suite) Test_Autofix_skip(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--only", "few", "--autofix")
mklines := t.SetUpFileMkLines("filename",
"VAR=\t111 222 333 444 555 \\",
"666")
lines := mklines.lines
fix := lines.Lines[0].Autofix()
fix.Warnf("Many.")
fix.Explain(
"Explanation.")
// None of the following actions has any effect because of the --only option above.
fix.Replace("111", "___")
fix.ReplaceAfter(" ", "222", "___")
fix.ReplaceAt(0, 0, "VAR", "NEW")
fix.InsertAbove("above")
fix.InsertBelow("below")
fix.Delete()
fix.Custom(func(showAutofix, autofix bool) {})
fix.Apply()
SaveAutofixChanges(lines)
t.CheckOutputEmpty()
t.CheckFileLines("filename",
"VAR=\t111 222 333 444 555 \\",
"666")
t.CheckEquals(fix.RawText(), ""+
"VAR=\t111 222 333 444 555 \\\n"+
"666\n")
}
func (s *Suite) Test_Autofix_assertRealLine(c *check.C) {
t := s.Init(c)
line := NewLineEOF("filename.mk")
fix := line.Autofix()
fix.Warnf("Warning.")
t.ExpectAssert(func() { fix.Replace("from", "to") })
}
func (s *Suite) Test_SaveAutofixChanges__file_removed(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
lines := t.SetUpFileLines("subdir/file.txt",
"line 1")
_ = os.RemoveAll(t.File("subdir").String())
fix := lines.Lines[0].Autofix()
fix.Warnf("Should start with an uppercase letter.")
fix.Replace("line", "Line")
fix.Apply()
SaveAutofixChanges(lines)
t.CheckOutputMatches(
"AUTOFIX: ~/subdir/file.txt:1: Replacing \"line\" with \"Line\".",
`ERROR: ~/subdir/file.txt.pkglint.tmp: Cannot write: .*`)
}
func (s *Suite) Test_SaveAutofixChanges__file_busy_Windows(c *check.C) {
t := s.Init(c)
if runtime.GOOS != "windows" {
return
}
t.SetUpCommandLine("--autofix")
lines := t.SetUpFileLines("subdir/file.txt",
"line 1")
// As long as the file is kept open, it cannot be overwritten or deleted.
openFile, err := os.OpenFile(t.File("subdir/file.txt").String(), 0, 0666)
defer func() { assertNil(openFile.Close(), "") }()
t.CheckNil(err)
fix := lines.Lines[0].Autofix()
fix.Warnf("Should start with an uppercase letter.")
fix.Replace("line", "Line")
fix.Apply()
SaveAutofixChanges(lines)
t.CheckOutputMatches(
"AUTOFIX: ~/subdir/file.txt:1: Replacing \"line\" with \"Line\".",
`ERROR: ~/subdir/file.txt.pkglint.tmp: Cannot overwrite with autofixed content: .*`)
}
// This test covers the highly unlikely situation in which a file is loaded
// by pkglint, and just before writing the autofixed content back, another
// process takes the file and replaces it with a directory of the same name.
//
// 100% code coverage sometimes requires creativity. :)
func (s *Suite) Test_SaveAutofixChanges__cannot_overwrite(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
lines := t.SetUpFileLines("file.txt",
"line 1")
t.CheckNil(os.RemoveAll(t.File("file.txt").String()))
t.CheckNil(os.MkdirAll(t.File("file.txt").String(), 0777))
fix := lines.Lines[0].Autofix()
fix.Warnf("Should start with an uppercase letter.")
fix.Replace("line", "Line")
fix.Apply()
SaveAutofixChanges(lines)
t.CheckOutputMatches(
"AUTOFIX: ~/file.txt:1: Replacing \"line\" with \"Line\".",
`ERROR: ~/file.txt.pkglint.tmp: Cannot overwrite with autofixed content: .*`)
}
func (s *Suite) Test_SaveAutofixChanges(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
lines := t.SetUpFileLines("example.txt",
"line1 := value1",
"line2 := value2",
"line3 := value3")
fix := lines.Lines[1].Autofix()
fix.Warnf("Something's wrong here.")
fix.Replace("lin", "XXX")
fix.Replace("e2 ", "XXX")
fix.Apply()
SaveAutofixChanges(lines)
t.CheckOutputLines(
"AUTOFIX: ~/example.txt:2: Replacing \"lin\" with \"XXX\".",
"AUTOFIX: ~/example.txt:2: Replacing \"e2 \" with \"XXX\".")
t.CheckFileLines("example.txt",
"line1 := value1",
"XXXXXX:= value2",
"line3 := value3")
}
func (s *Suite) Test_SaveAutofixChanges__no_changes_necessary(c *check.C) {
t := s.Init(c)
t.SetUpCommandLine("--autofix")
lines := t.SetUpFileLines("DESCR",
"Line 1",
"Line 2")
fix := lines.Lines[0].Autofix()
fix.Warnf("Dummy warning.")
fix.Replace("X", "Y")
fix.Apply()
// Since nothing has been effectively changed,
// nothing needs to be saved.
SaveAutofixChanges(lines)
// And therefore, no AUTOFIX action must appear in the log.
t.CheckOutputEmpty()
}
// RawText returns the raw text of the fixed line, including line ends.
// This may differ from the original text when the --show-autofix
// or --autofix options are enabled.
func (fix *Autofix) RawText() string {
var text strings.Builder
for _, above := range fix.above {
text.WriteString(above)
}
for _, modified := range fix.texts {
text.WriteString(modified)
}
for _, below := range fix.below {
text.WriteString(below)
}
return text.String()
}