Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/zsh-n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths:
- "zi.zsh"
- "lib/**"
- "tests/annex-unregister.zsh"
- "tests/archive-extraction.zsh"
- "tests/completion-refresh.zsh"
- "tests/hook-ownership.zsh"
Expand All @@ -33,6 +34,7 @@ on:
paths:
- "zi.zsh"
- "lib/**"
- "tests/annex-unregister.zsh"
- "tests/archive-extraction.zsh"
- "tests/completion-refresh.zsh"
- "tests/hook-ownership.zsh"
Expand Down Expand Up @@ -230,6 +232,17 @@ jobs:
- name: Test scheduler idle behavior
run: zsh -f tests/scheduler-idle.zsh

annex-unregister:
name: Annex Unregister
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 annex unregister
run: zsh -f tests/annex-unregister.zsh

archive-extraction:
name: Archive extraction
runs-on: ubuntu-latest
Expand Down
100 changes: 100 additions & 0 deletions tests/annex-unregister.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/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-annex-unregister-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}/first" \
"${temp_root}/second" || fail "create isolated environment"

builtin print -r -- 'builtin print -r -- first-loaded' \
> "${temp_root}/first/first.plugin.zsh" || fail "write the first plug-in"
builtin print -r -- 'builtin print -r -- second-loaded' \
> "${temp_root}/second/second.plugin.zsh" || fail "write the second 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" \
zsh -f <<'ZSH' || fail "an unregistered annex hook still affects later plug-in loads"
builtin emulate -R zsh
setopt pipe_fail

builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1
.zi-prepare-home || return 1

# Unregistering something never registered is a no-op, not an error.
@zi-unregister-annex never-registered hook:before-load-2 || {
builtin print -u2 -r -- "unregistering an absent annex reported an error"
return 1
}

probe_annex_handler() { return 0; }
@zi-register-annex probe-annex hook:before-load-2 probe_annex_handler '' ''

# The Plugin Standard unload contract has the annex remove its own handler.
# Without a matching unregister the ZI_EXTS entry survives, the before-load
# dispatch calls a name that no longer exists, and the resulting 127 is folded
# into the return value and shifts the argument list for the rest of the
# session.
@zi-unregister-annex probe-annex hook:before-load-2
unfunction probe_annex_handler

typeset output status_first status_second
output="$(zi load "${ZI_TEST_ROOT}/first" 2>&1)"
status_first=$?
[[ $status_first -eq 0 ]] || {
builtin print -u2 -r -- "loading after unregister returned ${status_first}: ${output}"
return 1
}
[[ $output == *first-loaded* ]] || {
builtin print -u2 -r -- "the first plug-in did not load: ${output}"
return 1
}

# A second load proves the argument list was not shifted by the first.
output="$(zi load "${ZI_TEST_ROOT}/second" 2>&1)"
status_second=$?
[[ $status_second -eq 0 ]] || {
builtin print -u2 -r -- "the second load returned ${status_second}: ${output}"
return 1
}
[[ $output == *second-loaded* ]] || {
builtin print -u2 -r -- "the second plug-in did not load: ${output}"
return 1
}

# The registry itself no longer holds the entry.
typeset key found=0
for key in "${(@k)ZI_EXTS}"; do
[[ ${ZI_EXTS[$key]} == *probe_annex_handler* ]] && found=1
done
(( found == 0 )) || {
builtin print -u2 -r -- "a ZI_EXTS entry for the unregistered annex survived"
return 1
}
ZSH

builtin print -r -- "ok - an unregistered annex hook is removed and later plug-in loads are unaffected"
43 changes: 43 additions & 0 deletions zi.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,49 @@ builtin setopt no_aliases
ZI_EXTS[ice-mods]="${ZI_EXTS[ice-mods]}${icemods:+|}${(j:|:)${(@)${(@s:|:)icemods}/(#b)(#s)(?)/$index-$match[1]}}"
}
} # ]]]
# FUNCTION: @zi-unregister-annex. [[[
# Removes the registrations made by @zi-register-annex and @zi-register-hook for
# one annex and one hook type, so an annex can remove its handler as the Zsh
# Plugin Standard unload contract expects without leaving Zi dispatching to a
# function that no longer exists.
#
# A stale registration is not inert. The before-load dispatch calls the stored
# handler unconditionally, a missing function returns 127, and `127 & 1' is true,
# so Zi folds the error into its return value and shifts its argument list on
# every later plug-in load in the session.
#
# Unregistering something that was never registered is a no-op.
#
# Note: the ice-modifier list each registration contributed to
# ZI_EXTS[ice-mods] and ZI_EXTS2[ice-mods] is not unwound. Registration appends
# to a joined string without recording which annex contributed which entry, so
# removing one annex's share would need that association to be recorded first.
# A leftover ice name is recognised but dispatches to nothing, which is
# harmless; the dispatch entry removed here is the part that corrupts loads.
@zi-unregister-annex() {
builtin emulate -LR zsh ${=${options[xtrace]:#off}:+-o xtrace}
builtin setopt extended_glob no_bang_hist typeset_silent

local name="$1" type="$2" ___key
local -a ___arr ___doomed

for ___key in "${(@k)ZI_EXTS}"; do
[[ $___key == (seqno|ice-mods) ]] && continue
___arr=( "${(Q)${(z@)ZI_EXTS[$___key]}[@]}" )
[[ ${___arr[3]} == $name && ${___arr[4]} == $type ]] && ___doomed+=( "$___key" )
done
(( ${#___doomed} )) && unset "ZI_EXTS[${^___doomed[@]}]"

___doomed=( )
for ___key in "${(@k)ZI_EXTS2}"; do
[[ $___key == (seqno|ice-mods) ]] && continue
___arr=( "${(Q)${(z@)ZI_EXTS2[$___key]}[@]}" )
[[ ${___arr[3]} == $name && ${___arr[4]} == $type ]] && ___doomed+=( "$___key" )
done
(( ${#___doomed} )) && unset "ZI_EXTS2[${^___doomed[@]}]"

return 0
} # ]]]
# FUNCTION: @zi-register-hook. [[[
# Registers the z-annex inside Zi.
@zi-register-hook() {
Expand Down
Loading