Skip to content

Conversation

davidbarratt
Copy link

@davidbarratt davidbarratt commented Aug 21, 2025

This PR fixes compatibility issues with newer versions of nikic/php-parser (specifically ^4.19) by addressing strict return type checking in NodeVisitor methods, and adds support for processing files with identical names from different directories.

Changes Made

Dependency Updates (from previous PR #1)

  • Updated PHP requirement to ^7.4 || ^8.0 for PHP 8.x support
  • Updated nikic/php-parser to ^4.0
  • Updated symfony/console to ^5.0 || ^6.0
  • Updated symfony/finder to ^5.4 || ^6.0
  • Updated php-stubs/wordpress-stubs to ^6.0

NodeVisitor Fixes

  • Fixed return type violations: Changed bare return; statements to return null; in Categorize::leaveNode() method
  • Removed improper REMOVE_NODE usage: The NodeTraverser::REMOVE_NODE constant can only be used when traversing arrays, not individual nodes
  • Added proper condition checks: Enhanced the condition check before calling addDefineConstantNames() to prevent undefined property access errors

Filename Conflict Resolution

  • Added path-based unique naming: Generated output filenames now include a path hash to prevent conflicts when processing files with identical basenames from different directories
  • Updated filename generation logic: The getFileName() method now accepts a path hash parameter and prepends it to output filenames when needed
  • Maintained backward compatibility: Files in the root directory continue to use simple names without path hashes

Problem Solved

The original code was throwing errors like:

leaveNode() returned invalid value of type integer
define() declaration has no constant name  

These errors occurred because:

  1. Newer php-parser versions are stricter about return types from visitor methods
  2. The REMOVE_NODE constant was being used incorrectly
  3. The define() detection logic wasn't properly validating node structure

Additionally, when processing multiple files with the same basename from different directories (e.g., vendor/package1/stubs.php and vendor/package2/stubs.php), output files would overwrite each other.

New Filename Format

For files in subdirectories, output filenames now include a 7-character hash of the relative path:

Before:

  • exclude-stubs-functions.php (conflicting files would overwrite)

After:

  • exclude-abc1234-stubs-functions.php (from vendor/package1/stubs.php)
  • exclude-def5678-stubs-functions.php (from vendor/package2/stubs.php)

Testing

The fixes have been tested against WordPress core files (1000+ PHP files) and successfully generate exclusion lists without errors. All existing tests have been updated to account for the new filename format.

Backward Compatibility

All changes maintain backward compatibility with existing php-parser versions and PHP 7.4+. Files processed from the root directory continue to use the original naming scheme.

🤖 Generated with Claude Code

suasgn and others added 5 commits November 6, 2022 21:14
- Fix return type violations in NodeVisitor methods by changing bare `return;` to `return null;`
- Remove improper use of NodeTraverser::REMOVE_NODE in Filter visitor
- Add proper condition checks before calling addDefineConstantNames() to prevent undefined property errors
- These changes ensure compatibility with php-parser ^4.19 while maintaining backward compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Handle different scalar node types in addDefineConstantNames to prevent
"Undefined property: PhpParser\Node\Scalar\Encapsed::$value" warnings.
Skip interpolated strings as constant names cannot be reliably extracted.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Generate unique output filenames by prepending path hash when processing
files with identical basenames from different directory structures. This
prevents filename conflicts when processing multiple stub files.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@davidbarratt
Copy link
Author

I also found out that if you have to files with the same name, in different folders, this script just overrides it. I added a hash of the path to fix that, which seemed like the most bc way to fix it.

@calvinalkan
Copy link
Member

calvinalkan commented Sep 4, 2025

Hey @davidbarratt

Thanks for the PR.


I think the filename hashing will be a BC break in the more commonly used WordPress lists?

See for example here in the Google Webstories plugin:

https://github.com/GoogleForCreators/web-stories-wp/blob/main/scoper.inc.php#L13

I think a much more sensible approach here is to just throw an exception for now if two files have the same name?

Can you share your configuration? Since you have full control over stub names, can't you just rename them in the config?

Maybe I'm missing an obvious use case here.

@davidbarratt
Copy link
Author

I'm open to alternatives here, but basically I'm running it on the entire WordPress & WooCommerce code base, and because of that it has a ton of files that are the same. Maybe I'm just doing it wrong? lol.

Here is the config:

<?php
/**
 * Generate Excludes configuration.
 *
 * @package Flex
 */

declare(strict_types=1);

use PhpParser\ParserFactory;
use Snicco\PhpScoperExcludes\Option;
use Symfony\Component\Finder\Finder;

return array(
	Option::EMULATE_PHP_VERSION => Option::PHP_8_0,
	Option::OUTPUT_DIR          => __DIR__ . '/excludes',
	Option::FILES               => array(
		Finder::create()->files()
				->in( array( __DIR__ . '/web/wp', __DIR__ . '/web/wp-content/plugins/woocommerce' ) )
				->name( '*.php' ),
	),
	Option::PREFER_PHP_VERSION  => ParserFactory::PREFER_PHP7,
);

@davidbarratt
Copy link
Author

Maybe it could throw an error by default and there is an option to opt-into the hashing behavior? honestly I would have been fine if it just merged into single file(s) too.

@davidbarratt
Copy link
Author

I'm building them into these arrays anyways, so it would be better, imho, if I just had these three files:
https://github.com/JoinFlexHealth/woocommerce/blob/62fe1e212c70cb79def1784072ffff0716db5ab2/scoper.inc.php#L29-L58

@calvinalkan
Copy link
Member

calvinalkan commented Sep 4, 2025

on the entire WordPress & WooCommerce [...]. Maybe I'm just doing it wrong? lol.

Yes, I think so. For WordPress Core, you'd just use our package directly:

https://github.com/snicco/php-scoper-wordpress-excludes

It uses lib as a dependency and publishes new WordPress exclusions via a CI/CD Cron when a new WP version is tagged. Generated lists are in this directory (Check the Google repo for usage).

For WooCommerce, you'd use pre-generated stub files, not the entire codebase (though, that also works by accident I guess).

There already exists:

https://github.com/php-stubs/woocommerce-stubs

Generated with (https://github.com/php-stubs/generator)

@davidbarratt
Copy link
Author

My concern was that someone might use a function in Woo or WordPress and not realize that they also need to update the stubs. It seems to work just fine on the entire code base other than overriding the files if it encounters one with the same name which feels like a bug.

@calvinalkan
Copy link
Member

calvinalkan commented Sep 4, 2025

and not realize that they also need to update the stubs.

Why would they need to update the stubs?

To clarify, this file here is generated code. It contains all WordPress core functions that exist, not only the ones you use.

You can use the latest tagged version of it (which is in sync with the WordPress version), and you'll have all your functions excluded correctly.

Check out this scoper config from Google's webstories plugin:

https://github.com/GoogleForCreators/web-stories-wp/blob/3af9f8ca0b84a72a313ad28ae627b423f97bd590/scoper.inc.php#L15

It shows the correct usage of the package.

@davidbarratt
Copy link
Author

Why would they need to update the stubs?

  1. WordPress or Woo adds a new function
  2. Engineer on our team uses new function
  3. Engineer doesn't update the stubs, in which the new function gets (incorrectly) namespaced and the plugin breaks.

@davidbarratt
Copy link
Author

Check out this scoper config from Google's webstories plugin:

Google seems to suffer from the same exact pitfall. It's fine as long as you keep the stubs in sync, otherwise it will break.

@calvinalkan
Copy link
Member

calvinalkan commented Sep 4, 2025

I assume your plugin always supports the latest stable WooCommerce/WordPress versions?

In your dev/build repository, you can require-dev this:

"sniccowp/php-scoper-wordpress-excludes": "^6.0"

And use those pre-generated stubs in phpscoper (like in Google's config).

It's fine as long as you keep the stubs in sync, otherwise it will break.

That package is automatically synced.

In your build script, you could run:

# Get latest WordPress stubs, if any.
composer update sniccowp/php-scoper-wordpress-excludes

# Optional, if you want to be ultra safe (overkill imo)
# Compare the composer version of sniccowp/php-scoper-wordpress-excludes to the latest WordPress version. 

# do the build

Check out these two files to see how everything is always kept in sync with WP versions:

@davidbarratt
Copy link
Author

So to clarify, this isn't really true then?

It's however not a requirement to use stubs files. Any PHP code will work.

@calvinalkan
Copy link
Member

No, it's true. But it will take a lot more time, and you'll run into the issue with the directories.

Since I can't add hashing filename without breaking all consumer packages, I'd recommend that you generate the stubs on the fly (or on demand based on change conditions) in your build step with:

https://github.com/php-stubs/generator

@calvinalkan calvinalkan self-assigned this Sep 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants