diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 934a28b..a1343b3 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -27,6 +27,7 @@ on: - "tests/snippet-directory-mirror.zsh" - "tests/source-hygiene.zsh" - "tests/subst-nesting.zsh" + - "tests/unload-hook-dispatch.zsh" - "tests/version-reporting.zsh" pull_request: paths: @@ -50,6 +51,7 @@ on: - "tests/snippet-directory-mirror.zsh" - "tests/source-hygiene.zsh" - "tests/subst-nesting.zsh" + - "tests/unload-hook-dispatch.zsh" - "tests/version-reporting.zsh" workflow_dispatch: {} @@ -98,6 +100,17 @@ jobs: zsh -fc "zcompile ${ZSH_FILE}"; rc=$? ls -al "${ZSH_FILE}.zwc"; exit "$rc" + unload-hook-dispatch: + name: Unload Hook Dispatch + 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 unload hook dispatch + run: zsh -f tests/unload-hook-dispatch.zsh + version-reporting: name: Version reporting runs-on: ubuntu-latest diff --git a/lib/zsh/autoload.zsh b/lib/zsh/autoload.zsh index b7f4d58..43623ab 100755 --- a/lib/zsh/autoload.zsh +++ b/lib/zsh/autoload.zsh @@ -897,7 +897,26 @@ ZI[EXTENDED_GLOB]="" # Call the Zsh Plugin's Standard *_plugin_unload function # - (( ${+functions[${plugin}_plugin_unload]} )) && ${plugin}_plugin_unload + # The Plugin Standard derives this name from the plug-in's name, and $plugin + # was used verbatim. Two kinds of plug-in could therefore never be reached: + # + # - a hyphenated repository, because ADR-0020's namespace rules require the + # shell prefix to use underscores, so `zsh-eza' can define + # `zsh_eza_plugin_unload' or be unloadable by Zi, but not both; + # - a plug-in loaded by path, whose $plugin is the absolute directory, so + # `/path/to/thing_plugin_unload' names nothing. + # + # Try the literal name first so plug-ins already defining it keep working, + # then the underscore form, then the same pair derived from the directory + # basename, which is the closest analogue of a repository name for a + # path-loaded plug-in. Call the first that exists; the Standard defines a + # single unload function per plug-in. + local ___base="${plugin:t}" ___cand + local -a ___cand_names + ___cand_names=( "$plugin" "${plugin//-/_}" "$___base" "${___base//-/_}" ) + for ___cand ( ${(u)___cand_names[@]} ) { + (( ${+functions[${___cand}_plugin_unload]} )) && { "${___cand}_plugin_unload"; break; } + } # # Call the code provided by the Zsh Plugin Standard @zsh-plugin-run-on-unload diff --git a/tests/unload-hook-dispatch.zsh b/tests/unload-hook-dispatch.zsh new file mode 100644 index 0000000..9873199 --- /dev/null +++ b/tests/unload-hook-dispatch.zsh @@ -0,0 +1,90 @@ +#!/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 + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-unload-hook-test.XXXXXXXX")" || + fail "create temporary directory" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" \ + "${temp_root}/plain" \ + "${temp_root}/hyph-en" || fail "create isolated environment" + +# A plug-in whose directory name has no hyphen. Its unload function is named +# after the directory, which is the Plugin Standard shape. +builtin print -rl -- \ + 'plain_plugin_unload() { builtin print -r -- plain-hook-ran >> $ZI_TEST_LOG; }' \ + > "${temp_root}/plain/plain.plugin.zsh" || fail "write the plain plug-in" + +# A plug-in whose directory name has a hyphen. ADR-0020 namespace rules require +# the shell prefix to use underscores, so the only name it may define is the +# underscore form. Before the fix nothing looked for that name. +builtin print -rl -- \ + 'hyph_en_plugin_unload() { builtin print -r -- hyphen-hook-ran >> $ZI_TEST_LOG; }' \ + > "${temp_root}/hyph-en/hyph-en.plugin.zsh" || fail "write the hyphenated plug-in" + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + ZI_TEST_LOG="${temp_root}/hooks.log" \ + zsh -f <<'ZSH' || fail "the Plugin Standard unload function was not called" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 +: > "$ZI_TEST_LOG" + +zi load "${ZI_TEST_ROOT}/plain" >/dev/null 2>&1 +(( ${+functions[plain_plugin_unload]} )) || { + builtin print -u2 -r -- "the plain plug-in did not define its unload function" + return 1 +} +zi unload "${ZI_TEST_ROOT}/plain" >/dev/null 2>&1 + +zi load "${ZI_TEST_ROOT}/hyph-en" >/dev/null 2>&1 +(( ${+functions[hyph_en_plugin_unload]} )) || { + builtin print -u2 -r -- "the hyphenated plug-in did not define its unload function" + return 1 +} +zi unload "${ZI_TEST_ROOT}/hyph-en" >/dev/null 2>&1 + +typeset log="$(<$ZI_TEST_LOG)" + +# A path-loaded plug-in is identified as `%', so the name derived +# verbatim from the id can never match. The basename is the analogue of a +# repository name. +[[ $log == *plain-hook-ran* ]] || { + builtin print -u2 -r -- "the unload function of a path-loaded plug-in was not called: [$log]" + return 1 +} + +# The underscore form, which is the only name ADR-0020 permits a hyphenated +# plug-in to define. +[[ $log == *hyphen-hook-ran* ]] || { + builtin print -u2 -r -- "the underscore-named unload function was not called: [$log]" + return 1 +} +ZSH + +builtin print -r -- "ok - the Plugin Standard unload function is reached for hyphenated and path-loaded plug-ins"