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
master
韩天峰 4 days ago
parent 65a44c7f65
commit 101b9f6261
  1. 178
      cleanup-typephp-tmp.sh
  2. 6
      src/Parser/ArrayExpressionTrait.php
  3. 2
      src/Parser/AssignOpTrait.php
  4. 75
      tests/compiler/ref/array-value-write-dereferences-source.phpt

@ -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

@ -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 . ');';
}
}

@ -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 . ')';

@ -0,0 +1,75 @@
--TEST--
ordinary array writes copy referenced sources while explicit references stay linked
--FILE--
<?php
function main()
{
$source = 1;
$reference =& $source;
$appended = [];
$appended[] = $reference;
$keyed = [];
$keyed['value'] = $reference;
$literal = [$reference];
$mixedLiteral = ['value' => $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)
}
Loading…
Cancel
Save