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
28 changes: 28 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

if [[ ! -x vendor/bin/php-cs-fixer || ! -x vendor/bin/phpunit || ! -x vendor/bin/phpstan ]]; then
echo "Pre-commit checks require Composer dependencies. Run: composer install" >&2
exit 1
fi

if [[ -z "${OPENSTATSPEC_SPECIFICATION_DIR:-}" && -d "$repo_root/../specification" ]]; then
export OPENSTATSPEC_SPECIFICATION_DIR="$repo_root/../specification"
fi

check_dir="$(mktemp -d "${TMPDIR:-/tmp}/openstatspec-php-check.XXXXXX")"
cleanup() {
rm -rf "$check_dir"
}
trap cleanup EXIT

staged_tree="$(git write-tree)"
git -c core.autocrlf=false archive "$staged_tree" | tar -x -C "$check_dir"
mkdir "$check_dir/vendor"
cp -a "$repo_root/vendor/." "$check_dir/vendor/"

cd "$check_dir"
composer check
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ composer install
composer check
```

Install the tracked pre-commit hook once per clone:

```bash
bash tools/install-git-hooks.sh
```

`composer check` validates Composer configuration, lints PHP, checks style, runs PHPStan and runs PHPUnit. Use `composer fix` for safe style fixes, then rerun `composer check`.

GitHub Actions runs the regular suite on PHP 8.4 and 8.5. It also runs real
Expand Down
19 changes: 19 additions & 0 deletions src/Frontend/Spss/Ast/DeleteVariablesStatement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace OpenStatSpec\Frontend\Spss\Ast;

final readonly class DeleteVariablesStatement implements Statement
{
/** @param list<string> $variables */
public function __construct(
public int $lineNumber,
public array $variables,
) {}

public function line(): int
{
return $this->lineNumber;
}
}
20 changes: 20 additions & 0 deletions src/Frontend/Spss/Ast/StringStatement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace OpenStatSpec\Frontend\Spss\Ast;

final readonly class StringStatement implements Statement
{
/** @param list<string> $variables */
public function __construct(
public int $lineNumber,
public array $variables,
public int $width,
) {}

public function line(): int
{
return $this->lineNumber;
}
}
16 changes: 16 additions & 0 deletions src/Frontend/Spss/Binder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace OpenStatSpec\Frontend\Spss;

use OpenStatSpec\Frontend\Spss\Ast\ElseInput;
use OpenStatSpec\Frontend\Spss\Ast\DeleteVariablesStatement;
use OpenStatSpec\Frontend\Spss\Ast\ExecuteStatement;
use OpenStatSpec\Frontend\Spss\Ast\MissingInput;
use OpenStatSpec\Frontend\Spss\Ast\StringStatement;
use OpenStatSpec\Frontend\Spss\Ast\Program;
use OpenStatSpec\Frontend\Spss\Ast\RangeInput;
use OpenStatSpec\Frontend\Spss\Ast\RecodeStatement;
Expand All @@ -15,6 +17,8 @@
use OpenStatSpec\Frontend\Spss\Binding\BoundProgram;
use OpenStatSpec\Frontend\Spss\Binding\BoundRecode;
use OpenStatSpec\Frontend\Spss\Binding\BoundValueLabels;
use OpenStatSpec\Frontend\Spss\Binding\BoundCreateVariable;
use OpenStatSpec\Frontend\Spss\Binding\BoundDeleteVariable;
use OpenStatSpec\Frontend\Spss\Binding\BoundVariableLabel;

final class Binder
Expand All @@ -30,6 +34,18 @@ public function bind(string $datasetId, Program $program): BoundProgram
if ($statement instanceof ExecuteStatement) {
continue;
}
if ($statement instanceof StringStatement) {
foreach ($statement->variables as $variable) {
$bound[] = new BoundCreateVariable($variable, 'string', $statement->width);
}
continue;
}
if ($statement instanceof DeleteVariablesStatement) {
foreach ($statement->variables as $variable) {
$bound[] = new BoundDeleteVariable($variable);
}
continue;
}
if ($statement instanceof RecodeStatement) {
$targets = $statement->targets === [] ? $statement->sources : $statement->targets;
if (count($statement->sources) !== count($targets)) {
Expand Down
14 changes: 14 additions & 0 deletions src/Frontend/Spss/Binding/BoundCreateVariable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace OpenStatSpec\Frontend\Spss\Binding;

final readonly class BoundCreateVariable implements BoundStatement
{
public function __construct(
public string $variable,
public string $storageKind,
public int $declaredStringWidth,
) {}
}
10 changes: 10 additions & 0 deletions src/Frontend/Spss/Binding/BoundDeleteVariable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace OpenStatSpec\Frontend\Spss\Binding;

final readonly class BoundDeleteVariable implements BoundStatement
{
public function __construct(public string $variable) {}
}
16 changes: 16 additions & 0 deletions src/Frontend/Spss/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
use OpenStatSpec\Frontend\Spss\Binding\BoundRecode;
use OpenStatSpec\Frontend\Spss\Binding\BoundValueLabels;
use OpenStatSpec\Frontend\Spss\Binding\BoundVariableLabel;
use OpenStatSpec\Frontend\Spss\Binding\BoundCreateVariable;
use OpenStatSpec\Frontend\Spss\Binding\BoundDeleteVariable;
use OpenStatSpec\Transformation\Model\Action\AssignValueAction;
use OpenStatSpec\Transformation\Model\CreateVariableOperation;
use OpenStatSpec\Transformation\Model\DeleteVariableOperation;
use OpenStatSpec\Transformation\Model\Action\CopySourceAction;
use OpenStatSpec\Transformation\Model\Action\SetMissingAction;
use OpenStatSpec\Transformation\Model\RecodeAction;
Expand All @@ -45,6 +49,18 @@ public function compile(BoundProgram $program): TransformationPlan
{
$operations = [];
foreach ($program->statements as $statement) {
if ($statement instanceof BoundCreateVariable) {
$operations[] = new CreateVariableOperation(
$statement->variable,
$statement->storageKind,
$statement->storageKind === 'string' ? $statement->declaredStringWidth : null,
);
continue;
}
if ($statement instanceof BoundDeleteVariable) {
$operations[] = new DeleteVariableOperation($statement->variable);
continue;
}
if ($statement instanceof BoundRecode) {
$rules = [];
$hasElse = false;
Expand Down
44 changes: 44 additions & 0 deletions src/Frontend/Spss/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace OpenStatSpec\Frontend\Spss;

use OpenStatSpec\Frontend\Spss\Ast\DeleteVariablesStatement;
use OpenStatSpec\Frontend\Spss\Ast\ElseInput;
use OpenStatSpec\Frontend\Spss\Ast\ExecuteStatement;
use OpenStatSpec\Frontend\Spss\Ast\MissingInput;
Expand All @@ -16,6 +17,7 @@
use OpenStatSpec\Frontend\Spss\Ast\RecodeStatement;
use OpenStatSpec\Frontend\Spss\Ast\ScalarValue;
use OpenStatSpec\Frontend\Spss\Ast\SystemMissingInput;
use OpenStatSpec\Frontend\Spss\Ast\StringStatement;
use OpenStatSpec\Frontend\Spss\Ast\ValueInput;
use OpenStatSpec\Frontend\Spss\Ast\ValueLabel;
use OpenStatSpec\Frontend\Spss\Ast\ValueLabelGroup;
Expand Down Expand Up @@ -60,6 +62,11 @@ public function parseTokens(array $tokens): Program
$statements[] = $this->valueLabels($command);
} elseif ($this->matchKeyword('EXECUTE')) {
$statements[] = new ExecuteStatement($command->line);
} elseif ($this->matchKeyword('STRING')) {
$statements[] = $this->string($command);
} elseif ($this->matchKeyword('DELETE')) {
$this->consumeKeyword('VARIABLES', 'Expected VARIABLES after DELETE.');
$statements[] = $this->deleteVariables($command);
} else {
$this->fail($command, sprintf('Unsupported SPSS command %s.', $command->lexeme === '' ? '<end of input>' : $command->lexeme));
}
Expand Down Expand Up @@ -180,6 +187,43 @@ private function valueLabels(Token $command): ValueLabelsStatement
return new ValueLabelsStatement($command->line, $groups);
}

private function string(Token $command): StringStatement
{
$variables = [];
do {
$variable = $this->consumeIdentifier('Expected a variable name in STRING.')->lexeme;
if (strcasecmp($variable, 'TO') === 0) {
$this->fail($this->previous(), 'STRING variable ranges using TO are not supported.');
}
$variables[] = $variable;
} while ($this->check(TokenType::Identifier));
$this->consume(TokenType::LeftParenthesis, 'Expected a width declaration in STRING.');
$width = $this->consume(TokenType::Identifier, 'Expected a string width such as A20.')->lexeme;
if (preg_match('/\AA([1-9][0-9]*)\z/i', $width, $matches) !== 1) {
$this->fail($this->previous(), 'STRING width must use the SPSS A<n> form.');
}
$widthValue = (int) $matches[1];
if ($widthValue > 32767) {
$this->fail($this->previous(), 'STRING width must be at most 32767.');
}
$this->consume(TokenType::RightParenthesis, 'Expected ) after STRING width.');

return new StringStatement($command->line, $variables, $widthValue);
}

private function deleteVariables(Token $command): DeleteVariablesStatement
{
$variables = [];
do {
$variable = $this->consumeIdentifier('Expected a variable name in DELETE VARIABLES.')->lexeme;
if (strcasecmp($variable, 'TO') === 0) {
$this->fail($this->previous(), 'DELETE VARIABLES ranges using TO are not supported.');
}
$variables[] = $variable;
} while ($this->check(TokenType::Identifier));

return new DeleteVariablesStatement($command->line, $variables);
}
private function scalar(string $message): ScalarValue
{
if ($this->match(TokenType::String)) {
Expand Down
Loading