tests/devscripts/comment-scan.bats
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env bats
SCAN="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd -P)/devscripts/comment-scan"
scan() {
local f="$BATS_TEST_TMPDIR/x.go"
cat > "$f"
run "$SCAN" "$f"
}
scan_ts() {
local f="$BATS_TEST_TMPDIR/x.tsx"
cat > "$f"
run "$SCAN" "$f"
}
scan_as() {
local f="$BATS_TEST_TMPDIR/$1"
mkdir -p "$(dirname "$f")"
cat > "$f"
run "$SCAN" "$f"
}
assert_kept() {
[ "$status" -eq 0 ] || { echo "scanner failed: $output" >&2; return 1; }
[ -z "$output" ] || { echo "expected nothing rejected, got:" >&2; echo "$output" >&2; return 1; }
}
assert_rejected() {
[[ "$output" == *"$1"* ]] || {
echo "expected '$1' to be rejected, got:" >&2
echo "${output:-<nothing>}" >&2
return 1
}
}
@test "a doc comment on an exported declaration is kept" {
scan <<'EOF'
package pkg
// Work runs the job and returns ErrBusy when another holds the lock.
func Work() error { return nil }
EOF
assert_kept
}
@test "a doc comment on an unexported declaration is rejected" {
scan <<'EOF'
package pkg
// work is the unexported half.
func work() error { return nil }
EOF
assert_rejected "the unexported half"
}
@test "narration above a statement is rejected" {
scan <<'EOF'
package pkg
func Work() {
// fetch the user and check
get()
}
EOF
assert_rejected "fetch the user and check"
}
@test "a comment on an exported const group is kept" {
scan <<'EOF'
package pkg
// The sort directions a query may ask for.
const (
OrderAsc = "asc"
OrderDesc = "desc"
)
EOF
assert_kept
}
@test "a doc comment on an exported member is kept" {
scan <<'EOF'
package pkg
type Config struct {
// Timeout bounds the whole dial, not each attempt.
Timeout int
}
EOF
assert_kept
}
@test "a block comment is read whole, not just its opener" {
scan <<'EOF'
package utmp
/* At session start a record is written to UtmpxFile.
If a record with the same ID exists it is overwritten;
otherwise it is appended.
*/
type Utmpx struct {
Pid int32
}
EOF
assert_kept
}
@test "a block comment inside a function is rejected in full" {
scan <<'EOF'
package pkg
func Work() {
/* the retry loop backs off
because the peer rate limits */
retry()
}
EOF
assert_rejected "the retry loop backs off"
assert_rejected "because the peer rate limits"
}
@test "a licence header is kept" {
scan <<'EOF'
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package pkg
EOF
assert_kept
}
@test "a build directive is kept" {
scan <<'EOF'
//go:build docker
package pkg
EOF
assert_kept
}
@test "a suppression with a reason is kept" {
scan <<'EOF'
package pkg
func Work() {
//nolint:errcheck // the caller closes conn on this path
conn.Close()
}
EOF
assert_kept
}
@test "the rows of a raw string literal are not comments" {
scan <<'EOF'
package pkg
const banner = `
******************************
* Welcome to ShellHub *
******************************
`
EOF
assert_kept
}
@test "a doc comment detached by a blank line is still the declaration's" {
scan <<'EOF'
package pkg
// Work runs the job.
func Work() error { return nil }
EOF
assert_kept
}
@test "an eslint suppression is kept in TypeScript" {
scan_ts <<'EOF'
export function useThing() {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {}, []);
}
EOF
assert_kept
}
@test "a JSDoc block on an export is kept whole" {
scan_ts <<'EOF'
/**
* Formats the device label, falling back to the hostname.
*/
export function label(device: Device): string {
return device.name;
}
EOF
assert_kept
}
@test "a local const is not exported, so its doc goes" {
scan <<'EOF'
package pkg
func Work() {
// MaxWait bounds one retry, not the whole loop.
const MaxWait = time.Hour
}
EOF
assert_rejected "MaxWait bounds one retry"
scan_ts <<'EOF'
export function Row() {
// build the label and render
const label = make();
return label;
}
EOF
assert_rejected "build the label and render"
}
@test "an empty file and a file with no comments are both clean" {
scan <<'EOF'
package pkg
func Work() error { return nil }
EOF
assert_kept
scan <<'EOF'
EOF
assert_kept
}
@test "a lone asterisk in JSX is a required marker, not a JSDoc continuation" {
scan_ts <<'EOF'
export function Field() {
return (
<label>
Hostname
<span aria-label="required" className="text-accent-red">
*
</span>
</label>
);
}
EOF
assert_kept
}
@test "a JSDoc continuation is still rejected with the block that opens it" {
scan_ts <<'EOF'
/**
* Formats a duration.
*/
function format(ms: number) {
return ms;
}
EOF
assert_rejected "Formats a duration"
assert_rejected "/**"
}
@test "a triple-slash reference is a compiler directive, not a comment" {
scan_ts <<'EOF'
/// <reference types="vite/client" />
export const mode = import.meta.env.MODE;
EOF
assert_kept
}
@test "a vitest environment pragma is a directive" {
scan_ts <<'EOF'
// @vitest-environment node
import { readFileSync } from "node:fs";
EOF
assert_kept
}
@test "a doc comment on a method of an exported class is what the linter asks for" {
scan_ts <<'EOF'
export class LocalVaultBackend {
/**
* Reads the vault header, or null when no vault exists here.
*/
loadMeta(): Promise<VaultMeta | null> {
return Promise.resolve(null);
}
}
EOF
assert_kept
}
@test "a doc comment on a declaration the export list names is kept" {
scan_ts <<'EOF'
/**
* Edits an API key's name and role.
*/
function EditKeyDrawer() {
return null;
}
export default EditKeyDrawer;
EOF
assert_kept
}
@test "a doc comment on a declaration nothing exports is still rejected" {
scan_ts <<'EOF'
/**
* Builds the label.
*/
function buildLabel() {
return "";
}
EOF
assert_rejected "Builds the label"
}
@test "narration inside an exported class is still rejected" {
scan_ts <<'EOF'
export class Recorder {
start(): void {
// reset the clock before writing the header
this.startMs = 0;
}
}
EOF
assert_rejected "reset the clock"
}
@test "narration inside a method of an exported class is rejected" {
scan_ts <<'EOF'
export class Recorder {
finish(): void {
// write the trailer before closing so a truncated file still replays
this.write("");
}
}
EOF
assert_rejected "write the trailer"
}
@test "a JSX comment in a component is rejected" {
scan_ts <<'EOF'
export function Header() {
return (
<div>
{/* the title sits left of the actions */}
<h1>Devices</h1>
</div>
);
}
EOF
assert_rejected "the title sits left"
}
@test "a comment line in a workflow is rejected" {
scan_as ci.yml <<'EOF'
jobs:
test:
# run the suite on every push
runs-on: ubuntu-24.04
EOF
assert_rejected "run the suite on every push"
}
@test "the version after a pinned action is kept" {
scan_as ci.yml <<'EOF'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
EOF
assert_kept
}
@test "a comment inside a run script is rejected" {
scan_as ci.yml <<'EOF'
steps:
- name: Build
run: |
# build twice so the cache warms
make build
EOF
assert_rejected "build twice so the cache warms"
}
@test "a markdown heading inside a text block scalar is kept" {
scan_as template.yml <<'EOF'
body:
- type: markdown
attributes:
value: |
# Bug report
Describe what happened.
EOF
assert_kept
}
@test "a heredoc body inside a run script is kept" {
scan_as ci.yml <<'EOF'
steps:
- run: |
cat > summary.md <<'MD'
# Results
MD
echo done
EOF
assert_kept
}
@test "a comment after a block scalar ends is rejected" {
scan_as compose.yml <<'EOF'
services:
api:
description: >
# not a comment
# the gateway fronts every service
gateway:
image: caddy
EOF
assert_rejected "the gateway fronts every service"
[[ "$output" != *"not a comment"* ]]
}
@test "yaml tool directives are kept" {
scan_as ci.yml <<'EOF'
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
on:
pull_request_target: # zizmor: ignore[dangerous-triggers]
EOF
assert_kept
}
@test "a shell script keeps its shebang and suppressions and loses its narration" {
scan_as run.sh <<'EOF'
#!/usr/bin/env bash
# shellcheck disable=SC2086
set -eu
# wait for the api before seeding
seed
EOF
assert_rejected "wait for the api before seeding"
[[ "$output" != *"shellcheck"* && "$output" != *"#!/"* ]]
}
@test "a heredoc body in a shell script is kept" {
scan_as run.sh <<'EOF'
cat > notes.md <<-NOTES
# Release notes
NOTES
EOF
assert_kept
}
@test "a herestring does not open a heredoc" {
scan_as run.sh <<'EOF'
read -r a b <<< "$pair"
# split the pair
echo "$a"
EOF
assert_rejected "split the pair"
}
@test "a shell script without an extension is read by its shebang" {
scan_as bin/tool <<'EOF'
#!/bin/sh
# forward everything to compose
exec docker compose "$@"
EOF
assert_rejected "forward everything to compose"
}
@test "a file without an extension or a shell shebang is not scanned" {
scan_as bin/data <<'EOF'
# not a script
EOF
assert_kept
}
@test "a licence header in a shell script is kept" {
scan_as run.sh <<'EOF'
#!/bin/sh
# Copyright 2020 O.S. Systems
# SPDX-License-Identifier: Apache-2.0
echo ok
EOF
assert_kept
}
@test "a Dockerfile keeps its parser directive and RUN heredoc and loses its narration" {
scan_as Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine
# install tools first so the layer caches
RUN <<EOT
# not a Dockerfile comment
apk add curl
EOT
EOF
assert_rejected "install tools first"
[[ "$output" != *"syntax="* && "$output" != *"not a Dockerfile comment"* ]]
}
@test "a Makefile comment is rejected" {
scan_as Makefile <<'EOF'
# build everything
all:
go build ./...
EOF
assert_rejected "build everything"
}
@test "a CSS comment is rejected and a stylelint suppression kept" {
scan_as base.css <<'EOF'
/* stylelint-disable-next-line selector-class-pattern */
.Legacy { color: red; }
/*
* the shadow separates the sidebar
*/
.seam { box-shadow: none; }
EOF
assert_rejected "the shadow separates the sidebar"
[[ "$output" != *"stylelint"* ]]
}
@test "an Astro markup comment is rejected, every line of it" {
scan_as Page.astro <<'EOF'
---
/** Props the page takes from its route. */
export interface Props { title: string }
---
<!--
keep the hero above the fold
-->
<h1>{Astro.props.title}</h1>
EOF
assert_rejected "keep the hero above the fold"
assert_rejected "<!--"
[[ "$output" != *"Props the page takes"* ]]
}
@test "a comment carrying a template action is kept" {
scan_as install.sh <<'EOF'
#!/bin/sh
# Overridden variables from Go template: {{.Overrides}}
echo ok
EOF
assert_kept
}
@test "the doc comment on an Astro component's Props is kept" {
scan_as Link.astro <<'EOF'
---
/**
* The one control a page sends its reader to.
*/
interface Props {
href: string;
}
---
<a href={Astro.props.href}><slot /></a>
EOF
assert_kept
}
@test "--language names the language a file is read as" {
run "$SCAN" --language .github/workflows/qa.yml
[ "$output" = yaml ]
run "$SCAN" --language docs/README.md
[ -z "$output" ]
}
repo() {
REPO="$BATS_TEST_TMPDIR/repo"
git init -q -b master "$REPO"
git -C "$REPO" config user.email bats@example.com
git -C "$REPO" config user.name bats
cd "$REPO"
}
commit() {
git add -A
git commit -q -m "$1"
}
@test "--diff reports a comment the branch adds and fails" {
repo
printf 'jobs:\n test:\n runs-on: ubuntu-24.04\n' > ci.yml
commit base
git checkout -q -b feature
printf 'jobs:\n # explain the job\n test:\n runs-on: ubuntu-24.04\n' > ci.yml
commit feature
run "$SCAN" --diff master
[ "$status" -eq 1 ]
[[ "$output" == *"ci.yml:2: # explain the job"* ]]
}
@test "--diff leaves a comment the base already had" {
repo
printf '#!/bin/sh\n# old narration\necho a\n' > run.sh
commit base
git checkout -q -b feature
printf '#!/bin/sh\n# old narration\necho a\necho b\n' > run.sh
commit feature
run "$SCAN" --diff master
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "--diff checks uncommitted and untracked files" {
repo
printf 'package pkg\n' > x.go
commit base
printf 'package pkg\n\nfunc f() {\n\t// narrate the call\n\tg()\n}\n' > x.go
printf '.a {\n /* explain the rule */\n color: red;\n}\n' > new.css
run "$SCAN" --diff master
[ "$status" -eq 1 ]
[[ "$output" == *"x.go:4:"*"narrate the call"* ]]
[[ "$output" == *"new.css:2:"*"explain the rule"* ]]
}
@test "--diff annotates each finding under GitHub Actions" {
repo
printf 'all:\n\ttrue\n' > Makefile
commit base
printf '# build everything\nall:\n\ttrue\n' > Makefile
GITHUB_ACTIONS=true run "$SCAN" --diff master
[ "$status" -eq 1 ]
[[ "$output" == *"::error file=Makefile,line=1::Comment not allowed: # build everything"* ]]
}
@test "--diff without a merge base explains what to fetch" {
repo
printf 'a: 1\n' > x.yml
commit base
run "$SCAN" --diff origin/master
[ "$status" -eq 2 ]
[[ "$output" == *"no merge base with origin/master"* ]]
}
@test "--help prints the usage and no argument is an error" {
run "$SCAN" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: devscripts/comment-scan --diff"* ]]
run "$SCAN"
[ "$status" -eq 2 ]
}