abuild-lint

A linting utility for Alpine Linux APKBUILDs

git clone https://git.8pit.net/abuild-lint.git

  1package main
  2
  3import (
  4	"fmt"
  5	"io/ioutil"
  6	"os"
  7	"sort"
  8	"strconv"
  9	"strings"
 10	"testing"
 11)
 12
 13const name = "Testinput"
 14
 15var reader *os.File
 16var writer *os.File
 17
 18type Msg struct {
 19	l uint
 20	c uint
 21	s string
 22}
 23
 24type byPos []string
 25
 26func (p byPos) Len() int {
 27	return len(p)
 28}
 29
 30func (p byPos) Swap(i, j int) {
 31	p[i], p[j] = p[j], p[i]
 32}
 33
 34func (p byPos) Less(i, j int) bool {
 35	linei := p[i]
 36	linej := p[j]
 37
 38	li, ci, _ := parseLine(linei)
 39	if li == 0 {
 40		return true
 41	}
 42
 43	lj, cj, _ := parseLine(linej)
 44	if li == lj {
 45		return ci < cj
 46	}
 47
 48	return li < lj
 49}
 50
 51func setup() {
 52	var err error
 53	reader, writer, err = os.Pipe()
 54	if err != nil {
 55		panic(err)
 56	}
 57}
 58
 59func parseLine(line string) (uint, uint, string) {
 60	sep := strings.Index(line, ":")
 61	if line[sep+1] == ' ' {
 62		return 0, 0, line[sep+2:]
 63	}
 64	index := sep + 1
 65
 66	lineLen := strings.Index(line[index:], ":")
 67	lineInfo := line[index : index+lineLen]
 68
 69	l, err := strconv.ParseUint(lineInfo, 10, 16)
 70	if err != nil {
 71		panic(err)
 72	}
 73	index += lineLen + 1
 74
 75	columLen := strings.Index(line[index:], ":")
 76	columnInfo := line[index : index+columLen]
 77
 78	c, err := strconv.ParseUint(columnInfo, 10, 16)
 79	if err != nil {
 80		panic(err)
 81	}
 82
 83	index += columLen + 2
 84	return uint(l), uint(c), line[index:]
 85}
 86
 87func newLinter(input string) *Linter {
 88	reader := strings.NewReader(input)
 89	abuild, err := Parse(reader, name)
 90	if err != nil {
 91		panic(err)
 92	}
 93
 94	linter := Linter{f: abuild, w: writer}
 95	return &linter
 96}
 97
 98func expMsg(t *testing.T, msgs ...Msg) {
 99	writer.Close() // Write EOF
100	defer setup()
101
102	data, err := ioutil.ReadAll(reader)
103	if err != nil {
104		t.Fatal("ioutil.ReadAll failed:", err)
105	}
106
107	str := string(data)
108	lines := strings.Split(str[0:len(str)-1], "\n")
109	if len(msgs) != len(lines) {
110		t.Fatalf("Expected %d violations, got %d",
111			len(msgs), len(lines))
112	}
113
114	// Output of some linter functions is non-deterministic,
115	// e.g. lintLocalVariables → sort output by token position.
116	sort.Sort(byPos(lines))
117
118	for n, m := range msgs {
119		line, column, text := parseLine(lines[n])
120		if line != m.l {
121			t.Fatalf("expFail: Line didn't match, expected %d - got %d", m.l, line)
122		}
123		if column != m.c {
124			t.Fatalf("expFail: Column didn't match, expected %d - got %d", m.c, column)
125		}
126		if text != m.s {
127			t.Fatalf("expFail Expected string %q - got %q", m.s, text)
128		}
129	}
130}
131
132func TestLintComments(t *testing.T) {
133	input := `#barfoo
134#
135# foobar
136#	bazbar
137#foobaz`
138
139	l := newLinter(input)
140	l.lintComments()
141
142	expMsg(t,
143		Msg{1, 1, badCommentPrefix},
144		Msg{4, 1, badCommentPrefix},
145		Msg{5, 1, badCommentPrefix})
146}
147
148func TestLintAddressComments(t *testing.T) {
149	input := `# foo: foo bar <foo@bar.com>
150# faz:
151# foo:
152# foo:
153# foo:foo bar <foo@bar.com>
154# foo: …`
155
156	l := newLinter(input)
157	n, addrs := l.lintAddressComments(" foo:")
158	if n != 5 || len(addrs) != 1 || !l.v {
159		t.Fail()
160	}
161
162	if addrs[0].a.String() != "\"foo bar\" <foo@bar.com>" {
163		t.Fatalf("Expected %q - got %q", "foo",
164			addrs[0].a.String())
165	}
166
167	expMsg(t,
168		Msg{3, 1, missingAddress},
169		Msg{4, 1, missingAddress},
170		Msg{5, 1, noAddressSeparator},
171		Msg{6, 1, invalidAddress})
172}
173
174func TestLintMaintainerAndContributors(t *testing.T) {
175	t.Run("missingMaintainer", func(t *testing.T) {
176		l := newLinter("")
177		l.lintMaintainerAndContributors()
178		expMsg(t, Msg{0, 0, missingMaintainer})
179	})
180
181	t.Run("emptyMaintainer", func(t *testing.T) {
182		l := newLinter("# Maintainer:")
183		l.lintMaintainerAndContributors()
184		expMsg(t, Msg{1, 1, missingAddress})
185	})
186
187	t.Run("tooManyMaintainers", func(t *testing.T) {
188		l := newLinter(`# Maintainer: A <a@a>
189# Maintainer: B <b@b>`)
190		l.lintMaintainerAndContributors()
191		expMsg(t, Msg{2, 1, tooManyMaintainers})
192	})
193
194	t.Run("maintainerAfterAssign", func(t *testing.T) {
195		l := newLinter(`pkgname=foo
196# Maintainer: A <a@b>`)
197		l.lintMaintainerAndContributors()
198		expMsg(t, Msg{2, 1, maintainerAfterAssign})
199	})
200
201	t.Run("wrongAddrCommentOrder", func(t *testing.T) {
202		l := newLinter(`# Maintainer: A <a@b>
203# Contributor: B <b@c>`)
204		l.lintMaintainerAndContributors()
205		expMsg(t, Msg{2, 1, wrongAddrCommentOrder})
206	})
207
208	t.Run("repeatedAddrComment", func(t *testing.T) {
209		l := newLinter(`# Contributor: A <a@b>
210# Contributor: A <a@b>
211# Maintainer: M <m@m>`)
212		l.lintMaintainerAndContributors()
213		expMsg(t, Msg{2, 1, repeatedAddrComment})
214	})
215
216	t.Run("oneMaintainer", func(t *testing.T) {
217		l := newLinter("# Maintainer: J <a@k>")
218		l.lintMaintainerAndContributors()
219		if l.v {
220			t.Fail()
221		}
222	})
223
224	t.Run("oneMaintainerAndContributors", func(t *testing.T) {
225		l := newLinter(`# Contributor: A <a@a>
226# Contributor: B <b@b>
227# Maintainer: C <c@c>`)
228		l.lintMaintainerAndContributors()
229		if l.v {
230			t.Fail()
231		}
232	})
233}
234
235func TestListGlobalVariables(t *testing.T) {
236	input := `pkgname=foobar
237foo=42
238_foo=9001
239__foo=bar
240export ENV=23`
241
242	l := newLinter(input)
243	l.lintGlobalVariables()
244
245	expMsg(t,
246		Msg{2, 1, fmt.Sprintf(invalidGlobalVar, "foo")},
247		Msg{4, 1, fmt.Sprintf(invalidGlobalVar, "__foo")})
248}
249
250func TestLintUnusedVariables(t *testing.T) {
251	input := `pkgname=foobar
252_foo=23
253_bar=42
254f1() {
255foo=lol
256}
257f2() {
258echo $_bar
259}
260f3() {
261FOO=bar make
262}
263f4() {
264export ENV=42
265}`
266
267	l := newLinter(input)
268	l.lintUnusedVariables()
269
270	expMsg(t,
271		Msg{2, 1, fmt.Sprintf(variableUnused, "_foo")},
272		Msg{5, 1, fmt.Sprintf(variableUnused, "foo")})
273}
274
275func TestLintGlobalCmdSubsts(t *testing.T) {
276	input := `pkgname=bar
277_bar=$(ls)
278f1() {
279local v1=${_bar}
280}
281_baz=$(cp -h)
282f2() {
283local v2=${_baz}
284}
285_baz=${foo} bar`
286
287	l := newLinter(input)
288	l.lintGlobalCmdSubsts()
289
290	expMsg(t,
291		Msg{2, 6, cmdSubstInGlobalVar},
292		Msg{6, 6, cmdSubstInGlobalVar})
293}
294
295func TestLintLocalVariables(t *testing.T) {
296	input := `f1() {
297foo=123
298}
299f2() {
300local foo=123
301}
302f3() {
303local bar=456
304}
305f4() {
306for foobar in "a" "b" "c"; do echo "$foobar"; done
307}
308f5() {
309export foo="bar"; echo "$foo"
310}
311VARFORCALLEXPR=23 ls`
312
313	l := newLinter(input)
314	l.lintLocalVariables()
315
316	expMsg(t,
317		Msg{2, 1, fmt.Sprintf(nonLocalVariable, "foo")},
318		Msg{11, 5, fmt.Sprintf(nonLocalVariable, "foobar")})
319}
320
321func TestLintParamExpression(t *testing.T) {
322	input := `# foobar
323foo=${pkgname}
324bar=$foo
325# barfoo
326foo=${pkgname##.*}
327foo=${foobar}foobar
328foo=${foobar}.$barfoo`
329
330	l := newLinter(input)
331	l.lintParamExpression()
332
333	expMsg(t,
334		Msg{2, 5, fmt.Sprintf(trivialLongParamExp, "pkgname", "pkgname")},
335		Msg{7, 5, fmt.Sprintf(trivialLongParamExp, "foobar", "foobar")})
336}
337
338func TestVariablePlacement(t *testing.T) {
339	input := `sha512sums=foobar
340myfunc() {
341echo myfunc
342}
343pkgname=barfoo`
344
345	l := newLinter(input)
346	l.lintMetadataPlacement()
347
348	expMsg(t,
349		Msg{1, 1, fmt.Sprintf(metadataAfterFunc, "sha512sums")},
350		Msg{5, 1, fmt.Sprintf(metadataBeforeFunc, "pkgname")})
351}
352
353func TestLintRequiredMetadata(t *testing.T) {
354	t.Run("allVarsDefined", func(t *testing.T) {
355		input := `pkgname=foobar
356pkgver=1337
357pkgrel=2342
358pkgdesc="foobar"
359url=http://example.org
360arch=all
361license=MIT
362sha512sums=1234`
363
364		l := newLinter(input)
365		l.lintRequiredMetadata()
366		if l.v {
367			t.Fail()
368		}
369	})
370
371	t.Run("missingOneVar", func(t *testing.T) {
372		input := `pkgname=foobar
373pkgrel=2342
374pkgdesc="foobar"
375url=http://example.org
376arch=all
377license=MIT
378sha512sums=1234`
379
380		l := newLinter(input)
381		l.lintRequiredMetadata()
382
383		expMsg(t,
384			Msg{0, 0, fmt.Sprintf(missingMetadata, "pkgver")})
385	})
386}
387
388func TestLintFunctionOrder(t *testing.T) {
389	t.Run("wrongFuncOrder", func(t *testing.T) {
390		input := `package() {
391}
392build() {
393}`
394
395		l := newLinter(input)
396		l.lintFunctionOrder()
397
398		expMsg(t,
399			Msg{1, 1, fmt.Sprintf(wrongFuncOrder, "package", "build")})
400	})
401
402	t.Run("rightFuncOrder", func(t *testing.T) {
403		input := `prepare() {
404}
405build() {
406}
407check() {
408}
409package() {
410}`
411
412		l := newLinter(input)
413		l.lintFunctionOrder()
414
415		if l.v {
416			t.Fail()
417		}
418	})
419}
420
421func TestLintBashisms(t *testing.T) {
422	input := `[[ -e "$builddir" ]] && foo=bar
423bar=*(foo bar)
424echo >(true)
425let x
426declare -A foobar
427readonly x
428typeset -r x
429nameref foo
430echo ${#foo}
431select s in foo bar baz; do
432	echo $s
433done
434function f() {
435return 1
436}`
437
438	l := newLinter(input)
439	l.lintBashisms()
440
441	expMsg(t,
442		Msg{1, 1, fmt.Sprintf(forbiddenBashism, "test clause")},
443		Msg{2, 5, fmt.Sprintf(forbiddenBashism, "extended globbing expression")},
444		Msg{3, 6, fmt.Sprintf(forbiddenBashism, "process substitution")},
445		Msg{4, 1, fmt.Sprintf(forbiddenBashism, "let clause")},
446		Msg{5, 1, fmt.Sprintf(forbiddenBashism, "declare")},
447		Msg{6, 1, fmt.Sprintf(forbiddenBashism, "readonly")},
448		Msg{7, 1, fmt.Sprintf(forbiddenBashism, "typeset")},
449		Msg{8, 1, fmt.Sprintf(forbiddenBashism, "nameref")},
450		Msg{9, 6, fmt.Sprintf(forbiddenBashism, "advanced parameter expression")},
451		Msg{10, 1, fmt.Sprintf(forbiddenBashism, "select clause")},
452		Msg{13, 1, fmt.Sprintf(forbiddenBashism, "non-POSIX function declaration")})
453}
454
455func TestMain(m *testing.M) {
456	setup()
457	os.Exit(m.Run())
458}