Ctrl K
ComunidadGuías…

devscripts/comment-scan

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -euo pipefail

DIRECTIVE='^[[:space:]]*//(go:|nolint)|^[[:space:]]*///[[:space:]]*<reference|eslint-disable|@(vitest|jest)-environment|@ts-(expect-error|ignore|nocheck)|prettier-ignore|istanbul ignore|Code generated|stylelint-disable|markdownlint-(disable|enable)|vale (on|off)'
HASH_DIRECTIVE='^[[:space:]]*#[[:space:]]*(shellcheck[[:space:]]|hadolint[[:space:]]|zizmor:|nosemgrep|yaml-language-server:|syntax=|escape=|check=|prettier-ignore|codespell:ignore|bats[[:space:]]+(file_tags|test_tags)=)|[{][{].*[}][}]'
LICENCE='Copyright|SPDX-License-Identifier|All rights reserved|Use of this source code|Licensed under'

usage() {
  cat <<'USAGE'
Usage: devscripts/comment-scan --diff [<base>]
       devscripts/comment-scan <file>
       devscripts/comment-scan --language <file>

Reports the comments the ShellHub code style does not allow. Two kinds stay:
a doc comment on an exported declaration (what revive's exported rule and
jsdoc/require-jsdoc ask for), and a linter suppression or tool directive with
its reason. Name the thing in code instead, and put the reason a name cannot
carry in the commit message, where git blame reaches it.

--diff [<base>]     Check only the lines added since the merge base with <base>
                    (default origin/master), committed or not, plus untracked
                    files. Prints <path>:<line>: <text> and exits 1 on a finding.
                    This is the check CI runs on a pull request.
<file>              Print <line>:<text> for every disallowed comment in <file>.
--language <file>   Print the language <file> is read as, or nothing.

Languages: Go, TypeScript/JavaScript (and JSX), Astro, HTML, CSS, YAML, shell,
bats, Dockerfiles and Makefiles. A script without an extension is read by its
shebang.
USAGE
}

language() {
  case "${1##*/}" in
    *.go) echo go ;;
    *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) echo ts ;;
    *.astro|*.html) echo markup ;;
    *.css) echo css ;;
    *.yml|*.yaml) echo yaml ;;
    *.sh|*.bash|*.bats|*.zsh|*.mk|Makefile|GNUmakefile|Dockerfile|Dockerfile.*|*.Dockerfile|*.dockerfile|Containerfile)
      echo shell ;;
    *.*) ;;
    *)
      if [[ -f "$1" ]] && head -n 1 "$1" | grep -qE '^#!.*[/ ](ba|da|k|z)?sh([[:space:]]|$)'; then
        echo shell
      fi
      ;;
  esac
}

scan_hash() {
  awk -v yaml="$2" -v directive="$HASH_DIRECTIVE" -v licence="$LICENCE" '
    function indent_of(s,   t) {
      t = s
      sub(/[^[:space:]].*$/, "", t)
      gsub(/\t/, "  ", t)
      return length(t)
    }
    function is_comment(i) {
      return !opaque[i] && src[i] ~ /^[[:space:]]*#/
    }
    { src[NR] = $0 }
    END {
      scalar = -1
      for (i = 1; i <= NR; i++) {
        line = src[i]
        if (heredoc != "") {
          opaque[i] = 1
          t = line
          gsub(/^[[:space:]]+|[[:space:]]+$/, "", t)
          if (t == heredoc) heredoc = ""
          continue
        }
        if (scalar >= 0) {
          if (line ~ /^[[:space:]]*$/ || indent_of(line) > scalar) {
            if (!script) { opaque[i] = 1; continue }
          } else {
            scalar = -1
          }
        }
        if (line ~ /^[[:space:]]*#/) continue
        if (yaml && scalar < 0 && line ~ /:[[:space:]]*[|>][-+0-9]*[[:space:]]*(#.*)?$/) {
          scalar = indent_of(line)
          key = line
          sub(/^[[:space:]]*(-[[:space:]]+)?/, "", key)
          sub(/[[:space:]]*:.*$/, "", key)
          script = (key == "run" || key == "script" || key == "command" || key == "entrypoint")
          continue
        }
        if (match(line, /<<-?[[:space:]]*["\047]?[A-Za-z_][A-Za-z0-9_]*/) && substr(line, RSTART - 1, 1) != "<") {
          heredoc = substr(line, RSTART, RLENGTH)
          sub(/^<<-?[[:space:]]*["\047]?/, "", heredoc)
        }
      }
      for (i = 1; i <= NR; i++) {
        if (!is_comment(i)) continue
        if (i == 1 && src[i] ~ /^#!/) continue
        if (src[i] ~ directive) continue
        for (j = i; j <= NR && is_comment(j); j++) continue
        for (s = i; s >= 1 && is_comment(s); s--) continue
        licensed = 0
        for (m = s + 1; m < j; m++) if (src[m] ~ licence) licensed = 1
        if (licensed) continue
        print i ":" src[i]
      }
    }
  ' "$1"
}

scan_slash() {
  awk -v ts="$2" -v markup="$3" -v directive="$DIRECTIVE" -v licence="$LICENCE" '
    function is_comment(s) {
      return s ~ /^[[:space:]]*\/\// || s ~ /^[[:space:]]*\/\*/ ||
        (ts && s ~ /^[[:space:]]*\{\/\*/) || (markup && s ~ /^[[:space:]]*<!--/)
    }
    function mark_blocks(   i, open, closer) {
      for (i = 1; i <= NR; i++) {
        if (open) { blocked[i] = 1; if (src[i] ~ closer) open = 0; continue }
        if ((src[i] ~ /^[[:space:]]*\/\*/ || (ts && src[i] ~ /^[[:space:]]*\{\/\*/)) && src[i] !~ /\*\//) {
          blocked[i] = 1; open = 1; closer = "\\*/"
        } else if (markup && src[i] ~ /^[[:space:]]*<!--/ && src[i] !~ /-->/) {
          blocked[i] = 1; open = 1; closer = "-->"
        }
      }
    }
    function indent_of(s,   t) {
      t = s
      sub(/[^[:space:]].*$/, "", t)
      gsub(/\t/, "  ", t)
      return length(t)
    }
    function declares(s) {
      if (s ~ /^package[[:space:]]/) return 1
      if (s ~ /^func[[:space:]]+(\([^)]*\)[[:space:]]*)?[A-Z]/) return 1
      if (s ~ /^(type|const|var)[[:space:]]+[A-Z]/) return 1
      if (s ~ /^(type|const|var)[[:space:]]*\(/) return 1
      if (ts && s ~ /^export[[:space:]]/) return 1
      if (ts && s ~ /^(function|class|const|let|var|interface|type|enum)[[:space:]]/) {
        name = s
        sub(/^(function|class|const|let|var|interface|type|enum)[[:space:]]+/, "", name)
        sub(/[^A-Za-z0-9_$].*$/, "", name)
        if (name in exported) return 1
      }
      return 0
    }
    function collect_exports(   i, line, names, n, k) {
      for (i = 1; i <= NR; i++) {
        line = src[i]
        if (line ~ /^export[[:space:]]+default[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*[[:space:]]*;?[[:space:]]*$/) {
          name = line
          sub(/^export[[:space:]]+default[[:space:]]+/, "", name)
          sub(/[^A-Za-z0-9_$].*$/, "", name)
          exported[name] = 1
          continue
        }
        if (line ~ /^export[[:space:]]*\{/) {
          names = line
          sub(/^export[[:space:]]*\{/, "", names)
          sub(/\}.*$/, "", names)
          n = split(names, k, ",")
          for (j2 = 1; j2 <= n; j2++) {
            name = k[j2]
            gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
            sub(/[[:space:]]+as[[:space:]]+.*$/, "", name)
            sub(/^type[[:space:]]+/, "", name)
            if (name != "") exported[name] = 1
          }
        }
      }
    }
    { src[NR] = $0 }
    END {
      mark_blocks()
      collect_exports()
      if (markup) exported["Props"] = 1
      for (i = 1; i <= NR; i++) {
        if (group == 2) {
          if (src[i] !~ /^[[:space:]]*$/ && indent_of(src[i]) == member_indent) member[i] = 1
          if (src[i] ~ /^[})]/) group = 0
        } else if (group) {
          member[i] = 1
          if (src[i] ~ /^[})]/) group = 0
        } else if (src[i] ~ /^(export[[:space:]]+)?(default[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]/) {
          group = 2
          member_indent = -1
        } else if (src[i] ~ /^(export[[:space:]]+)?(type|const|var|interface)[[:space:]]*([A-Z][^=]*)?[({][[:space:]]*$/) {
          group = 1
        }
        if (group == 2 && member_indent < 0 && src[i] !~ /^[[:space:]]*$/ && indent_of(src[i]) > 0) member_indent = indent_of(src[i])
      }
      for (i = 1; i <= NR; i++) {
        if (!(is_comment(src[i]) || blocked[i])) continue
        if (src[i] ~ directive) continue
        for (j = i; j <= NR && (is_comment(src[j]) || blocked[j]); j++) continue
        for (s = i; s >= 1 && (is_comment(src[s]) || blocked[s]); s--) continue
        licensed = 0
        for (m = s + 1; m < j; m++) if (src[m] ~ licence) licensed = 1
        if (licensed) continue
        for (d = j; d <= NR && src[d] ~ /^[[:space:]]*$/; d++) continue
        if (d <= NR && (declares(src[d]) || (member[d] && src[d] ~ /^[[:space:]]+([A-Z]|(static|async|get|set|readonly|private|public|protected)[[:space:]]|[a-z_$][A-Za-z0-9_$]*[<(])/))) continue
        print i ":" src[i]
      }
    }
  ' "$1"
}

scan_file() {
  case "$(language "$1")" in
    go|css) scan_slash "$1" 0 0 ;;
    ts) scan_slash "$1" 1 0 ;;
    markup) scan_slash "$1" 1 1 ;;
    yaml) scan_hash "$1" 1 ;;
    shell) scan_hash "$1" 0 ;;
  esac
}

added_lines() {
  awk '/^@@/ {
    match($0, /\+[0-9]+(,[0-9]+)?/)
    split(substr($0, RSTART + 1, RLENGTH - 1), r, ",")
    n = (2 in r) ? r[2] : 1
    for (i = 0; i < n; i++) print r[1] + i
  }'
}

annotation_escape() {
  local s=$1
  s=${s//'%'/'%25'}
  s=${s//$'\r'/'%0D'}
  s=${s//$'\n'/'%0A'}
  printf '%s' "$s"
}

scan_diff() {
  local base=$1 root merge_base found=0 path lines report n text
  root=$(git rev-parse --show-toplevel)
  cd "$root"
  merge_base=$(git merge-base "$base" HEAD) || {
    echo "comment-scan: no merge base with $base; fetch it first (git fetch origin)." >&2
    exit 2
  }

  while IFS= read -r path; do
    [[ -f "$path" && ! -L "$path" ]] || continue
    [[ -n "$(language "$path")" ]] || continue
    if git ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
      lines=$(git diff -U0 "$merge_base" -- "$path" | added_lines | tr '\n' ' ')
    else
      lines=$(awk '{ printf "%d ", NR }' "$path")
    fi
    [[ -n "${lines// /}" ]] || continue

    report=$(scan_file "$path" | awk -v nums="$lines" '
      BEGIN { split(nums, a, " "); for (i in a) want[a[i]] = 1 }
      { split($0, p, ":"); if (p[1] in want) print }')
    [[ -n "$report" ]] || continue

    while IFS= read -r line; do
      n=${line%%:*}
      text=${line#*:}
      text=${text#"${text%%[![:space:]]*}"}
      echo "$path:$n: $text"
      if [[ "${GITHUB_ACTIONS:-}" == true ]]; then
        echo "::error file=$(annotation_escape "$path"),line=$n::$(annotation_escape "Comment not allowed: $text")"
      fi
      found=$((found + 1))
    done <<<"$report"
  done < <({
    git diff --name-only --diff-filter=d "$merge_base" --
    git ls-files --others --exclude-standard
  } | sort -u)

  if ((found > 0)); then
    echo
    echo "$found comment(s) above are not allowed. Run devscripts/comment-scan --help for the rule."
    exit 1
  fi
}

case "${1:-}" in
  -h|--help) usage ;;
  --language) language "$2" ;;
  --diff) scan_diff "${2:-origin/master}" ;;
  "") usage >&2; exit 2 ;;
  *) scan_file "$1" ;;
esac