Skip to content

Commit ceb129a

Browse files
committed
Add PHPStan static analysis, minimal fixes
- Install PHPStan at level 0 - Fix few issues that PHPStan found: - Add missing return statement in Parser::_convertFunction() - Fix regex patterns by properly escaping $ characters - Replace unset() with null assignment for PHP 8.4 compatibility - Fix duplicate array keys in ptg definitions - Add unit tests to verify the fixes - Add dedicated static analysis workflow using PHP 8.2
1 parent 8dad9a3 commit ceb129a

File tree

8 files changed

+186
-10
lines changed

8 files changed

+186
-10
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
6060
- name: Install dependencies
6161
run: |
62-
composer remove --no-update --dev friendsofphp/php-cs-fixer
62+
composer remove --no-update --dev friendsofphp/php-cs-fixer phpstan/phpstan
6363
composer install --no-interaction --prefer-dist --no-progress
6464
6565
- name: Lint code
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Static Analysis
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
- master
9+
10+
jobs:
11+
phpstan:
12+
name: PHPStan
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup PHP
20+
uses: shivammathur/setup-php@v2
21+
with:
22+
php-version: 8.2
23+
coverage: none
24+
25+
- name: Cache Composer dependencies
26+
uses: actions/cache@v4
27+
with:
28+
path: ~/.composer/cache
29+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
30+
restore-keys: |
31+
${{ runner.os }}-composer-
32+
33+
- name: Install dependencies
34+
run: composer install --prefer-dist --no-progress --no-suggest
35+
36+
- name: Run PHPStan
37+
run: vendor/bin/phpstan analyse

Spreadsheet/Excel/Writer/Parser.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,13 @@ protected function _initializeHashes()
270270
'ptgRefNV' => 0x4C,
271271
'ptgAreaNV' => 0x4D,
272272
'ptgMemAreaNV' => 0x4E,
273-
'ptgMemNoMemN' => 0x4F,
273+
'ptgMemNoMemNV' => 0x4F,
274274
'ptgFuncCEV' => 0x58,
275275
'ptgNameXV' => 0x59,
276276
'ptgRef3dV' => 0x5A,
277277
'ptgArea3dV' => 0x5B,
278278
'ptgRefErr3dV' => 0x5C,
279-
'ptgAreaErr3d' => 0x5D,
279+
'ptgAreaErr3dV' => 0x5D,
280280
'ptgArrayA' => 0x60,
281281
'ptgFuncA' => 0x61,
282282
'ptgFuncVarA' => 0x62,
@@ -292,13 +292,13 @@ protected function _initializeHashes()
292292
'ptgRefNA' => 0x6C,
293293
'ptgAreaNA' => 0x6D,
294294
'ptgMemAreaNA' => 0x6E,
295-
'ptgMemNoMemN' => 0x6F,
295+
'ptgMemNoMemNA' => 0x6F,
296296
'ptgFuncCEA' => 0x78,
297297
'ptgNameXA' => 0x79,
298298
'ptgRef3dA' => 0x7A,
299299
'ptgArea3dA' => 0x7B,
300300
'ptgRefErr3dA' => 0x7C,
301-
'ptgAreaErr3d' => 0x7D
301+
'ptgAreaErr3dA' => 0x7D
302302
);
303303

304304
// Thanks to Michael Meeks and Gnumeric for the initial arg values.
@@ -671,6 +671,9 @@ protected function _convertFunction($token, $num_args)
671671
if ($args == -1) {
672672
return pack("CCv", $this->ptg['ptgFuncVarV'], $num_args, $this->_functions[$token][0]);
673673
}
674+
675+
// Default return for safety
676+
return pack("Cv", $this->ptg['ptgFuncV'], $this->_functions[$token][0]);
674677
}
675678

676679
/**

Spreadsheet/Excel/Writer/Worksheet.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ public function getData($cleanup = true)
754754
// Return data stored in memory
755755
if (isset($this->_data)) {
756756
$tmp = $this->_data;
757-
unset($this->_data);
757+
$this->_data = null; // Set to null instead of unset for PHP 8.4 compatibility
758758
if ($this->_using_tmpfile) {
759759
fseek($this->_filehandle, 0);
760760
}
@@ -1455,20 +1455,21 @@ protected function _substituteCellref($cell)
14551455
}
14561456

14571457
// Convert a cell range: 'A1:B7'
1458-
if (preg_match("/\$?([A-I]?[A-Z]\$?\d+):\$?([A-I]?[A-Z]\$?\d+)/", $cell, $match)) {
1458+
if (preg_match("/\\$?([A-I]?[A-Z]\\$?\\d+):\\$?([A-I]?[A-Z]\\$?\\d+)/", $cell, $match)) {
14591459
list($row1, $col1) = $this->_cellToRowcol($match[1]);
14601460
list($row2, $col2) = $this->_cellToRowcol($match[2]);
14611461
return (array($row1, $col1, $row2, $col2));
14621462
}
14631463

14641464
// Convert a cell reference: 'A1' or 'AD2000'
1465-
if (preg_match("/\$?([A-I]?[A-Z]\$?\d+)/", $cell)) {
1465+
if (preg_match("/\\$?([A-I]?[A-Z]\\$?\\d+)/", $cell, $match)) {
14661466
list($row1, $col1) = $this->_cellToRowcol($match[1]);
14671467
return (array($row1, $col1));
14681468
}
14691469

14701470
// TODO use real error codes
14711471
$this->raiseError("Unknown cell reference $cell", 0, PEAR_ERROR_DIE);
1472+
return array(0, 0); // Return default to satisfy static analysis
14721473
}
14731474

14741475
/**
@@ -1481,7 +1482,7 @@ protected function _substituteCellref($cell)
14811482
*/
14821483
protected function _cellToRowcol($cell)
14831484
{
1484-
preg_match("/\$?([A-I]?[A-Z])\$?(\d+)/",$cell,$match);
1485+
preg_match("/\\$?([A-I]?[A-Z])\\$?(\\d+)/",$cell,$match);
14851486
$col = $match[1];
14861487
$row = $match[2];
14871488

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"php-coveralls/php-coveralls": "^2.2",
4141
"phpunit/phpunit": ">=5 <10",
4242
"sanmai/phpunit-legacy-adapter": "^6 || ^8",
43-
"php-parallel-lint/php-parallel-lint": "^1.4"
43+
"php-parallel-lint/php-parallel-lint": "^1.4",
44+
"phpstan/phpstan": "^2.1"
4445
},
4546
"autoload": {
4647
"psr-0": {

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 0
3+
paths:
4+
- Spreadsheet
5+
- test
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Test\Spreadsheet\Excel\Writer;
4+
5+
use Spreadsheet_Excel_Writer_Parser;
6+
7+
class ParserTest extends \LegacyPHPUnit\TestCase
8+
{
9+
/**
10+
* Test that _convertFunction returns a value for all code paths
11+
*/
12+
public function testConvertFunctionReturnsValue()
13+
{
14+
$parser = new Spreadsheet_Excel_Writer_Parser(0, 0x0500);
15+
16+
// Initialize the parser properly
17+
$parser->_initializeHashes();
18+
19+
// Access protected method via reflection
20+
$method = new \ReflectionMethod($parser, '_convertFunction');
21+
$method->setAccessible(true);
22+
23+
// Test with a function that has fixed args (should return early)
24+
// TIME has 3 fixed arguments
25+
$result = $method->invoke($parser, 'TIME', 3);
26+
$this->assertNotEmpty($result);
27+
$this->assertInternalType('string', $result);
28+
29+
// Test variable args path - SUM has variable args
30+
$result = $method->invoke($parser, 'SUM', 2);
31+
$this->assertNotEmpty($result);
32+
$this->assertInternalType('string', $result);
33+
}
34+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace Test\Spreadsheet\Excel\Writer;
4+
5+
use Spreadsheet_Excel_Writer_Worksheet;
6+
use Spreadsheet_Excel_Writer_Workbook;
7+
8+
class WorksheetTest extends \LegacyPHPUnit\TestCase
9+
{
10+
private $workbook;
11+
private $worksheet;
12+
13+
public function setUp()
14+
{
15+
parent::setUp();
16+
$this->workbook = new Spreadsheet_Excel_Writer_Workbook('php://memory');
17+
$this->worksheet = new Spreadsheet_Excel_Writer_Worksheet(0x0500, 'Test', 0, 0, $this->workbook->_url_format);
18+
}
19+
20+
public function tearDown()
21+
{
22+
if ($this->workbook) {
23+
$this->workbook->close();
24+
}
25+
parent::tearDown();
26+
}
27+
28+
/**
29+
* Test that _substituteCellref handles regex with dollar signs correctly
30+
*/
31+
public function testSubstituteCellrefWithDollarSigns()
32+
{
33+
$method = new \ReflectionMethod($this->worksheet, '_substituteCellref');
34+
$method->setAccessible(true);
35+
36+
// Test absolute cell reference
37+
$result = $method->invoke($this->worksheet, '$A$1');
38+
$this->assertEquals(array(0, 0), $result);
39+
40+
// Test mixed references
41+
$result = $method->invoke($this->worksheet, 'A$1');
42+
$this->assertEquals(array(0, 0), $result);
43+
44+
$result = $method->invoke($this->worksheet, '$A1');
45+
$this->assertEquals(array(0, 0), $result);
46+
47+
// Test cell range with dollar signs
48+
$result = $method->invoke($this->worksheet, '$A$1:$B$2');
49+
$this->assertEquals(array(0, 0, 1, 1), $result);
50+
}
51+
52+
/**
53+
* Test that _cellToRowcol handles regex with dollar signs correctly
54+
*/
55+
public function testCellToRowcolWithDollarSigns()
56+
{
57+
$method = new \ReflectionMethod($this->worksheet, '_cellToRowcol');
58+
$method->setAccessible(true);
59+
60+
// Test with absolute reference
61+
$result = $method->invoke($this->worksheet, '$A$1');
62+
$this->assertEquals(array(0, 0), $result);
63+
64+
// Test with relative reference
65+
$result = $method->invoke($this->worksheet, 'B2');
66+
$this->assertEquals(array(1, 1), $result);
67+
68+
// Test with mixed reference
69+
$result = $method->invoke($this->worksheet, '$C3');
70+
$this->assertEquals(array(2, 2), $result);
71+
}
72+
73+
/**
74+
* Test that getData properly handles clearing _data property
75+
*/
76+
public function testGetDataClearsDataProperty()
77+
{
78+
// Access protected property
79+
$property = new \ReflectionProperty($this->worksheet, '_data');
80+
$property->setAccessible(true);
81+
82+
// Set some data
83+
$testData = 'test data';
84+
$property->setValue($this->worksheet, $testData);
85+
86+
// Call getData
87+
$result = $this->worksheet->getData();
88+
89+
// Check that data was returned
90+
$this->assertEquals($testData, $result);
91+
92+
// Check that _data is now null (not unset)
93+
$this->assertNull($property->getValue($this->worksheet));
94+
}
95+
}

0 commit comments

Comments
 (0)