From 101b9f6261f28f2fff66182d6dbecc46060685bd Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 22 Aug 2026 13:36:45 +0800 Subject: [PATCH] fix(parser): handle array value write dereferences correctly - Updated ArrayExpressionTrait to use setValue/appendValue methods when not by reference - Changed set/append calls to respect byRef flag in array expressions - Fixed assign operation to use appendValue instead of append for direct writes - Added comprehensive test case for array write reference behavior - Implemented proper dereferencing logic for array assignments - Added cleanup script for TypePHP temporary files with safety checks --- cleanup-typephp-tmp.sh | 178 ++++++++++++++++++ src/Parser/ArrayExpressionTrait.php | 6 +- src/Parser/AssignOpTrait.php | 2 +- ...array-value-write-dereferences-source.phpt | 75 ++++++++ 4 files changed, 258 insertions(+), 3 deletions(-) create mode 100755 cleanup-typephp-tmp.sh create mode 100644 tests/compiler/ref/array-value-write-dereferences-source.phpt diff --git a/cleanup-typephp-tmp.sh b/cleanup-typephp-tmp.sh new file mode 100755 index 00000000..f19e3020 --- /dev/null +++ b/cleanup-typephp-tmp.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly MIN_AGE_MINUTES=60 + +dry_run=false +tmp_root=/tmp + +usage() { + cat <<'EOF' +Usage: ./cleanup-typephp-tmp.sh [options] + +Remove inactive TypePHP temporary files and directories from /tmp. +An entry is skipped when it or any of its descendants was modified or +metadata-changed during the last 60 minutes. +Recognized prefixes: typephp-, typephp_, utils_test_, and phpx-windows. + +Options: + -n, --dry-run Show what would be removed without deleting anything + --tmp-dir DIR Use another temporary directory (primarily for testing) + -h, --help Show this help +EOF +} + +while (($# > 0)); do + case "$1" in + -n | --dry-run) + dry_run=true + ;; + --tmp-dir) + if (($# < 2)); then + echo "Error: --tmp-dir requires a directory." >&2 + exit 2 + fi + tmp_root=$2 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "Error: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [[ ! -d "$tmp_root" ]]; then + echo "Error: temporary directory does not exist: $tmp_root" >&2 + exit 1 +fi + +tmp_root=$(realpath -e -- "$tmp_root") +if [[ -z "$tmp_root" || "$tmp_root" == / ]]; then + echo "Error: refusing to use an unsafe temporary directory." >&2 + exit 1 +fi + +readonly tmp_root +readonly owner_uid=${SUDO_UID:-$(id -u)} + +format_size() { + local kib=$1 + awk -v kib="$kib" 'BEGIN { + if (kib >= 1048576) { + printf "%.2f GiB", kib / 1048576 + } else if (kib >= 1024) { + printf "%.2f MiB", kib / 1024 + } else { + printf "%d KiB", kib + } + }' +} + +entry_size_kib() { + local output + output=$(du -sk -- "$1" 2>/dev/null) || { + printf '0' + return + } + printf '%s' "${output%%$'\t'*}" +} + +has_recent_entry() { + local candidate=$1 + local recent + + # Check the complete tree. Looking only at the top-level directory mtime + # would miss writes to an existing file in a nested build directory. + if ! recent=$(find -P "$candidate" -xdev \ + \( -mmin "-${MIN_AGE_MINUTES}" -o -cmin "-${MIN_AGE_MINUTES}" \) \ + -printf '1' -quit 2>/dev/null); then + return 0 + fi + + [[ -n "$recent" ]] +} + +matched_count=0 +removed_count=0 +skipped_recent_count=0 +skipped_error_count=0 +total_kib=0 + +while IFS= read -r -d '' candidate; do + ((matched_count += 1)) + + # Keep the target constrained to one direct child of the selected root. + if [[ "$candidate" != "$tmp_root"/* || "${candidate%/*}" != "$tmp_root" ]]; then + echo "[skip unsafe] $candidate" >&2 + ((skipped_error_count += 1)) + continue + fi + + if has_recent_entry "$candidate"; then + echo "[skip recent] $candidate" + ((skipped_recent_count += 1)) + continue + fi + + size_kib=$(entry_size_kib "$candidate") + size=$(format_size "$size_kib") + + # The size scan can take noticeable time for a large build tree. Recheck + # freshness immediately before acting in case a compiler started using it. + if has_recent_entry "$candidate"; then + echo "[skip recent] $candidate" + ((skipped_recent_count += 1)) + continue + fi + + if $dry_run; then + echo "[would remove] $size $candidate" + ((removed_count += 1)) + ((total_kib += size_kib)) + continue + fi + + if [[ -d "$candidate" && ! -L "$candidate" ]]; then + if rm -rf --one-file-system -- "$candidate"; then + echo "[removed] $size $candidate" + ((removed_count += 1)) + ((total_kib += size_kib)) + else + echo "[skip error] failed to remove: $candidate" >&2 + ((skipped_error_count += 1)) + fi + elif rm -f -- "$candidate"; then + echo "[removed] $size $candidate" + ((removed_count += 1)) + ((total_kib += size_kib)) + else + echo "[skip error] failed to remove: $candidate" >&2 + ((skipped_error_count += 1)) + fi +done < <( + find -P "$tmp_root" -mindepth 1 -maxdepth 1 -uid "$owner_uid" \ + \( -name 'typephp-*' -o -name 'typephp_*' \ + -o -name 'utils_test_*' -o -name 'phpx-windows*' \) -print0 +) + +if $dry_run; then + action='would remove' +else + action='removed' +fi + +printf 'Summary: matched %d, %s %d (%s), skipped recent %d, errors %d.\n' \ + "$matched_count" "$action" "$removed_count" "$(format_size "$total_kib")" \ + "$skipped_recent_count" "$skipped_error_count" + +if ((skipped_error_count > 0)); then + exit 1 +fi diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index fb546174..c0486b80 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -289,9 +289,11 @@ trait ArrayExpressionTrait } elseif ($item->key) { $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); - $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.set(' . $key . ', ' . $value . ');'; + $method = $item->byRef ? 'set' : 'setValue'; + $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.' . $method . '(' . $key . ', ' . $value . ');'; } else { - $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.append(' . $value . ');'; + $method = $item->byRef ? 'append' : 'appendValue'; + $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.' . $method . '(' . $value . ');'; } } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 2878926b..da14e9d7 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -73,7 +73,7 @@ trait AssignOpTrait && $arrayType === Type::ARRAY && $this->canEmitDirectArrayWriteOperand($right) ) { - return $code . $array . '.append(' . $value . ')'; + return $code . $array . '.appendValue(' . $value . ')'; } $tmp = $this->addTmpVar(Type::VAR); return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')'; diff --git a/tests/compiler/ref/array-value-write-dereferences-source.phpt b/tests/compiler/ref/array-value-write-dereferences-source.phpt new file mode 100644 index 00000000..aa7669b2 --- /dev/null +++ b/tests/compiler/ref/array-value-write-dereferences-source.phpt @@ -0,0 +1,75 @@ +--TEST-- +ordinary array writes copy referenced sources while explicit references stay linked +--FILE-- + $reference, $reference]; + $explicitReference = [&$reference]; + + $reference = 2; + var_dump($appended, $keyed, $literal, $mixedLiteral, $explicitReference); + + $target = 10; + $targetArray = [&$target]; + $other = 20; + $otherReference =& $other; + $targetArray[0] = $otherReference; + $otherReference = 30; + var_dump($target, $targetArray, $other); + + $nestedSource = [&$otherReference]; + $nestedCopy = []; + $nestedCopy[] = $nestedSource[0]; + $otherReference = 40; + var_dump($nestedCopy, $nestedSource); +} +?> +--EXPECT-- +array(1) { + [0]=> + int(1) +} +array(1) { + ["value"]=> + int(1) +} +array(1) { + [0]=> + int(1) +} +array(2) { + ["value"]=> + int(1) + [0]=> + int(1) +} +array(1) { + [0]=> + &int(2) +} +int(20) +array(1) { + [0]=> + &int(20) +} +int(30) +array(1) { + [0]=> + int(30) +} +array(1) { + [0]=> + &int(40) +}