diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 34b59e7..934a28b 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -25,6 +25,7 @@ on: - "tests/fixtures/plugin-standard-callbacks/**" - "tests/self-update-reload.zsh" - "tests/snippet-directory-mirror.zsh" + - "tests/source-hygiene.zsh" - "tests/subst-nesting.zsh" - "tests/version-reporting.zsh" pull_request: @@ -47,6 +48,7 @@ on: - "tests/fixtures/plugin-standard-callbacks/**" - "tests/self-update-reload.zsh" - "tests/snippet-directory-mirror.zsh" + - "tests/source-hygiene.zsh" - "tests/subst-nesting.zsh" - "tests/version-reporting.zsh" workflow_dispatch: {} @@ -259,6 +261,17 @@ jobs: - name: Test message formatting run: zsh -f tests/message-formatting.zsh + source-hygiene: + name: Source Hygiene + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test source hygiene + run: zsh -f tests/source-hygiene.zsh + subst-nesting: name: Subst Nesting runs-on: ubuntu-latest diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index d136afd..b7f4d58 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -299,14 +299,6 @@ ZI[EXTENDED_GLOB]="" fi return 0 } # ]]] -# FUNCTION: .zi-at-eval [[[ -.zi-at-eval() { - local atclone="$2" atpull="$1" - integer retval - @zi-substitute atclone atpull - [[ $atpull = "%atclone" ]] && { eval "$atclone"; retval=$?; } || { eval "$atpull"; retval=$?; } - return $retval -} # ]]] # # Format functions @@ -1917,7 +1909,7 @@ ZI[EXTENDED_GLOB]="" +zi-message "{mmdsh}{happy} Zi{rst} » {info3}update took {num}$SECONDS{info3} seconds{rst}{…}" return "$retval" } # ]]] -# FUNCTION: .zi-update-in-parallel [[[ +# FUNCTION: .zi-update-all-parallel [[[ .zi-update-all-parallel() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} builtin setopt extended_glob warn_create_global typeset_silent no_short_loops no_monitor no_notify diff --git a/tests/source-hygiene.zsh b/tests/source-hygiene.zsh new file mode 100644 index 0000000..7324399 --- /dev/null +++ b/tests/source-hygiene.zsh @@ -0,0 +1,80 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# +# Structural checks over the shipped sources. Neither can be caught by a +# behavioural test: a duplicate definition whose copies agree behaves correctly +# until they drift, and a wrong header misleads only the next reader. Both are +# cheap to assert and expensive to rediscover. + +builtin emulate -R zsh +setopt extended_glob pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +builtin cd -q "$project_root" || fail "enter the project root" + +typeset -a sources +sources=( zi.zsh lib/zsh/*.zsh(N) ) +(( ${#sources} )) || fail "locate the shipped sources" + +# A top-level definition: a name in column one followed by `()'. Indented +# definitions are anonymous or nested helpers and are deliberately not counted. +typeset -r name_class='[A-Za-z_@.:+][A-Za-z0-9_@.:+-]#' + +typeset -A seen_in +typeset -a duplicates mismatches +typeset src name header defined +typeset -a lines +typeset -i i j + +for src in "${sources[@]}"; do + lines=( "${(@f)$(<$src)}" ) + + for (( i = 1; i <= ${#lines}; i++ )); do + # Every function is defined exactly once across the shipped sources. Both + # libraries are sourced conditionally and in different orders depending on + # the command, so a second definition does not reliably override the first: + # which one wins varies by code path. `.zi-at-eval' was defined in both + # lib/zsh/autoload.zsh and lib/zsh/install.zsh with matching behaviour and + # different wording, exactly the shape that becomes a heisenbug once the + # two drift. + if [[ ${lines[i]} == (#b)(${~name_class})[[:space:]]#'()'* ]]; then + name="${match[1]}" + if [[ -n ${seen_in[$name]} ]]; then + duplicates+=( "${name}: ${seen_in[$name]} and ${src}:${i}" ) + else + seen_in[$name]="${src}:${i}" + fi + continue + fi + + # Every `# FUNCTION: ' header names the function it precedes. + if [[ ${lines[i]} == (#b)'#'[[:space:]]#'FUNCTION:'[[:space:]]#(${~name_class})* ]]; then + header="${match[1]%.}" + for (( j = i + 1; j <= i + 6 && j <= ${#lines}; j++ )); do + [[ ${lines[j]} == (#b)(${~name_class})[[:space:]]#'()'* ]] || continue + defined="${match[1]}" + [[ $header == $defined ]] || + mismatches+=( "${src}:${i} header '${header}' precedes '${defined}'" ) + break + done + fi + done +done + +if (( ${#duplicates} )); then + builtin print -u2 -rl -- "functions defined more than once:" "${duplicates[@]}" + fail "a function is defined in more than one shipped source" +fi + +if (( ${#mismatches} )); then + builtin print -u2 -rl -- "FUNCTION headers naming a different function:" "${mismatches[@]}" + fail "a FUNCTION header does not match the function it precedes" +fi + +builtin print -r -- "ok - every function is defined once and every FUNCTION header matches (${#seen_in} functions)" diff --git a/zi.zsh b/zi.zsh index ea35881..f81e5b5 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2379,7 +2379,7 @@ builtin setopt no_aliases "the list of the {cmd}subcommands$bcol.{rst}" } } # ]]] -# FUNCTION: +zi-parse-opts. [[[ +# FUNCTION: .zi-parse-opts. [[[ .zi-parse-opts() { builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace} builtin setopt extended_glob typeset_silent no_short_loops rc_quotes no_auto_pushd