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() }