From 8df1b942aaef65ff9d861ce00a59ab2ff7baabe2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 26 Jun 2024 12:37:51 -0700 Subject: [PATCH] feat: add new client upgrade fixer (#143) --- .gitignore | 5 +- composer.json | 8 +- .../ClientUpgradeFixer/legacy_class_vars.php | 43 +++ .../ClientUpgradeFixer/legacy_inline_html.php | 48 ++++ .../legacy_kitchen_sink.php | 72 +++++ examples/ClientUpgradeFixer/legacy_mixins.php | 27 ++ .../legacy_multiple_clients.php | 28 ++ .../ClientUpgradeFixer/legacy_no_args.php | 11 + .../legacy_non_rpc_methods.php | 21 ++ .../legacy_optional_args.php | 32 +++ .../legacy_optional_args_array_keyword.php | 32 +++ .../legacy_optional_args_variable.php | 34 +++ .../legacy_required_and_optional_args.php | 35 +++ .../legacy_required_args.php | 20 ++ .../legacy_var_typehint.php | 60 ++++ .../legacy_vars_defined_elsewhere.php | 31 +++ .../ClientUpgradeFixer/new_class_vars.php | 51 ++++ .../ClientUpgradeFixer/new_inline_html.php | 55 ++++ .../ClientUpgradeFixer/new_kitchen_sink.php | 92 +++++++ examples/ClientUpgradeFixer/new_mixins.php | 34 +++ .../new_multiple_clients.php | 33 +++ examples/ClientUpgradeFixer/new_no_args.php | 13 + .../new_non_rpc_methods.php | 24 ++ .../ClientUpgradeFixer/new_optional_args.php | 40 +++ .../new_optional_args_array_keyword.php | 40 +++ .../new_optional_args_variable.php | 44 +++ .../new_required_and_optional_args.php | 47 ++++ .../ClientUpgradeFixer/new_required_args.php | 29 ++ .../ClientUpgradeFixer/new_var_typehint.php | 65 +++++ .../new_vars_defined_elsewhere.php | 38 +++ phpunit.xml.dist | 1 + .../ClientUpgradeFixer/ClientUpgradeFixer.php | 257 ++++++++++++++++++ src/Fixers/ClientUpgradeFixer/ClientVar.php | 180 ++++++++++++ src/Fixers/ClientUpgradeFixer/README.md | 114 ++++++++ .../ClientUpgradeFixer/RequestClass.php | 53 ++++ .../RequestVariableCounter.php | 30 ++ src/Fixers/ClientUpgradeFixer/RpcMethod.php | 222 +++++++++++++++ .../ClientUpgradeFixer/RpcParameter.php | 101 +++++++ .../ClientUpgradeFixer/UseStatement.php | 50 ++++ test/Fixers/ClientUpgradeFixerTest.php | 61 +++++ 40 files changed, 2178 insertions(+), 3 deletions(-) create mode 100644 examples/ClientUpgradeFixer/legacy_class_vars.php create mode 100644 examples/ClientUpgradeFixer/legacy_inline_html.php create mode 100644 examples/ClientUpgradeFixer/legacy_kitchen_sink.php create mode 100644 examples/ClientUpgradeFixer/legacy_mixins.php create mode 100644 examples/ClientUpgradeFixer/legacy_multiple_clients.php create mode 100644 examples/ClientUpgradeFixer/legacy_no_args.php create mode 100644 examples/ClientUpgradeFixer/legacy_non_rpc_methods.php create mode 100644 examples/ClientUpgradeFixer/legacy_optional_args.php create mode 100644 examples/ClientUpgradeFixer/legacy_optional_args_array_keyword.php create mode 100644 examples/ClientUpgradeFixer/legacy_optional_args_variable.php create mode 100644 examples/ClientUpgradeFixer/legacy_required_and_optional_args.php create mode 100644 examples/ClientUpgradeFixer/legacy_required_args.php create mode 100644 examples/ClientUpgradeFixer/legacy_var_typehint.php create mode 100644 examples/ClientUpgradeFixer/legacy_vars_defined_elsewhere.php create mode 100644 examples/ClientUpgradeFixer/new_class_vars.php create mode 100644 examples/ClientUpgradeFixer/new_inline_html.php create mode 100644 examples/ClientUpgradeFixer/new_kitchen_sink.php create mode 100644 examples/ClientUpgradeFixer/new_mixins.php create mode 100644 examples/ClientUpgradeFixer/new_multiple_clients.php create mode 100644 examples/ClientUpgradeFixer/new_no_args.php create mode 100644 examples/ClientUpgradeFixer/new_non_rpc_methods.php create mode 100644 examples/ClientUpgradeFixer/new_optional_args.php create mode 100644 examples/ClientUpgradeFixer/new_optional_args_array_keyword.php create mode 100644 examples/ClientUpgradeFixer/new_optional_args_variable.php create mode 100644 examples/ClientUpgradeFixer/new_required_and_optional_args.php create mode 100644 examples/ClientUpgradeFixer/new_required_args.php create mode 100644 examples/ClientUpgradeFixer/new_var_typehint.php create mode 100644 examples/ClientUpgradeFixer/new_vars_defined_elsewhere.php create mode 100644 src/Fixers/ClientUpgradeFixer/ClientUpgradeFixer.php create mode 100644 src/Fixers/ClientUpgradeFixer/ClientVar.php create mode 100644 src/Fixers/ClientUpgradeFixer/README.md create mode 100644 src/Fixers/ClientUpgradeFixer/RequestClass.php create mode 100644 src/Fixers/ClientUpgradeFixer/RequestVariableCounter.php create mode 100644 src/Fixers/ClientUpgradeFixer/RpcMethod.php create mode 100644 src/Fixers/ClientUpgradeFixer/RpcParameter.php create mode 100644 src/Fixers/ClientUpgradeFixer/UseStatement.php create mode 100644 test/Fixers/ClientUpgradeFixerTest.php diff --git a/.gitignore b/.gitignore index 2519988..2617e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ composer.lock vendor/ build/ -.php_cs.cache \ No newline at end of file +.php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache + diff --git a/composer.json b/composer.json index 6be63c1..eefcb4d 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ ], "autoload": { "psr-4": { + "Google\\Cloud\\Fixers\\": "src/Fixers/", "Google\\Cloud\\TestUtils\\": "src/TestUtils/", "Google\\Cloud\\Utils\\": "src/Utils/" } @@ -38,7 +39,10 @@ "google/gax": "^1.0.0", "paragonie/random_compat": ">=2", "phpunit/phpunit": "^9", - "friendsofphp/php-cs-fixer": "^3.17", - "phpspec/prophecy-phpunit": "^2.0" + "phpspec/prophecy-phpunit": "^2.0", + "friendsofphp/php-cs-fixer": "^3.21", + "google/cloud-dlp": "^1.10", + "google/cloud-storage": "^1.33", + "google/cloud-secret-manager": "^1.12" } } diff --git a/examples/ClientUpgradeFixer/legacy_class_vars.php b/examples/ClientUpgradeFixer/legacy_class_vars.php new file mode 100644 index 0000000..6658de7 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_class_vars.php @@ -0,0 +1,43 @@ +dlp = new DlpServiceClient(); + $this->secretmanager = new SecretManagerServiceClient(); + } + + public function callDlp() + { + $infoTypes = $this->dlp->listInfoTypes(); + } + + public function callSecretManager() + { + $secrets = $this->secretmanager->listSecrets('this/is/a/parent'); + } +} + +// Instantiate a wrapping object. +$wrapper = new ClientWrapper(); + +// these should update +$infoTypes = $wrapper->dlp->listInfoTypes(); +$secrets = $wrapper->secretmanager->listSecrets('this/is/a/parent'); diff --git a/examples/ClientUpgradeFixer/legacy_inline_html.php b/examples/ClientUpgradeFixer/legacy_inline_html.php new file mode 100644 index 0000000..d9f1062 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_inline_html.php @@ -0,0 +1,48 @@ +serializeToJsonString(), true), + JSON_PRETTY_PRINT + ); +} +?> + + + + + +

Google Cloud Sample App

+
+

List Secrets

+
+ listSecrets($parent) as $secret): ?> +
+ +
+ +

List DLP Jobs

+
+ listDlpJobs($parent) as $job): ?> +
+ +
+
+ + diff --git a/examples/ClientUpgradeFixer/legacy_kitchen_sink.php b/examples/ClientUpgradeFixer/legacy_kitchen_sink.php new file mode 100644 index 0000000..936401a --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_kitchen_sink.php @@ -0,0 +1,72 @@ +listInfoTypes(); + +// optional args array (variable form) +$dlp->listInfoTypes($foo); + +// required args variable +$dlp->createDlpJob($foo); + +// required args string +$dlp->createDlpJob('this/is/a/parent'); + +// required args array +$dlp->createDlpJob(['jobId' => 'abc', 'locationId' => 'def']); + +// required args variable and optional args array +$dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']); + +// required args variable and optional args variable +$dlp->createDlpJob($parent, $optionalArgs); + +// required args variable and optional args array with nested array +$job = $dlp->createDlpJob($parent, [ + 'inspectJob' => new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ]), + 'trailingComma' => true, +]); + +$projectId = 'my-project'; +$secretId = 'my-secret'; + +// Create the Secret Manager client. +$client = new SecretManagerServiceClient(); + +// Build the parent name from the project. +$parent = $client->projectName($projectId); + +// Create the parent secret. +$secret = $client->createSecret($parent, $secretId, + new Secret([ + 'replication' => new Replication([ + 'automatic' => new Automatic(), + ]), + ]) +); diff --git a/examples/ClientUpgradeFixer/legacy_mixins.php b/examples/ClientUpgradeFixer/legacy_mixins.php new file mode 100644 index 0000000..6382620 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_mixins.php @@ -0,0 +1,27 @@ +getIamPolicy($resource); + +// IAM conditions need at least version 3 +if ($policy->getVersion() != 3) { + $policy->setVersion(3); +} + +$binding = new Binding([ + 'role' => 'roles/spanner.fineGrainedAccessUser', + 'members' => [$iamMember], + 'condition' => new Expr([ + 'title' => $title, + 'expression' => sprintf("resource.name.endsWith('/databaseRoles/%s')", $databaseRole) + ]) +]); +$policy->setBindings([$binding]); +$secretManager->setIamPolicy($resource, $policy); diff --git a/examples/ClientUpgradeFixer/legacy_multiple_clients.php b/examples/ClientUpgradeFixer/legacy_multiple_clients.php new file mode 100644 index 0000000..8fe44f8 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_multiple_clients.php @@ -0,0 +1,28 @@ +listInfoTypes(); +$secrets = $secretmanager->listSecrets('this/is/a/parent'); + +// these shouldn't update +$operations = $longrunning->listOperations(); +$serviceAccount = $storage->getServiceAccount(); diff --git a/examples/ClientUpgradeFixer/legacy_no_args.php b/examples/ClientUpgradeFixer/legacy_no_args.php new file mode 100644 index 0000000..4b45383 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_no_args.php @@ -0,0 +1,11 @@ +listInfoTypes(); diff --git a/examples/ClientUpgradeFixer/legacy_non_rpc_methods.php b/examples/ClientUpgradeFixer/legacy_non_rpc_methods.php new file mode 100644 index 0000000..07f8a76 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_non_rpc_methods.php @@ -0,0 +1,21 @@ +dlpJobName('my-project', 'my-job'); + +// Call the "close" method +$job = $dlp->close(); + +// Call an RPC method +$job = $dlp->getDlpJob($jobName); + +// Call a non-existant method! +$job = $dlp->getJob($jobName); diff --git a/examples/ClientUpgradeFixer/legacy_optional_args.php b/examples/ClientUpgradeFixer/legacy_optional_args.php new file mode 100644 index 0000000..a6d3a44 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_optional_args.php @@ -0,0 +1,32 @@ +listInfoTypes($parent); + +// optional args array (inline array) +$job = $dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']); + +// optional args array (inline with nested arrays) +$job = $dlp->createDlpJob($parent, [ + 'inspectJob' => new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ]) +]); diff --git a/examples/ClientUpgradeFixer/legacy_optional_args_array_keyword.php b/examples/ClientUpgradeFixer/legacy_optional_args_array_keyword.php new file mode 100644 index 0000000..0178bc4 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_optional_args_array_keyword.php @@ -0,0 +1,32 @@ +listInfoTypes($args); + +// optional args array (inline array) +$job = $dlp->createDlpJob($parent, array('jobId' => 'abc', 'locationId' => 'def')); + +// optional args array (inline with nested arrays) +$job = $dlp->createDlpJob($parent, array( + 'inspectJob' => new InspectJobConfig(array( + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + )) +)); diff --git a/examples/ClientUpgradeFixer/legacy_optional_args_variable.php b/examples/ClientUpgradeFixer/legacy_optional_args_variable.php new file mode 100644 index 0000000..37aa6bc --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_optional_args_variable.php @@ -0,0 +1,34 @@ +listInfoTypes($parent); + +// optional args array (inline array) +$options = ['jobId' => 'abc', 'locationId' => 'def']; +$job = $dlp->createDlpJob($parent, $options); + +// optional args array (inline with nested arrays) +$options2 = [ + 'inspectJob' => new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ]) +]; +$job = $dlp->createDlpJob($parent, $options2); diff --git a/examples/ClientUpgradeFixer/legacy_required_and_optional_args.php b/examples/ClientUpgradeFixer/legacy_required_and_optional_args.php new file mode 100644 index 0000000..b980ec4 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_required_and_optional_args.php @@ -0,0 +1,35 @@ +createDlpJob($parent, $optionalArgs); + +// required args variable and optional args array +$dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']); + +// required args string and optional variable +$dlp->createDlpJob('path/to/parent', ['jobId' => 'abc', 'locationId' => 'def']); + +// required args variable and optional args array with nested array +$job = $dlp->createDlpJob($parent, [ + 'inspectJob' => new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ]) +]); diff --git a/examples/ClientUpgradeFixer/legacy_required_args.php b/examples/ClientUpgradeFixer/legacy_required_args.php new file mode 100644 index 0000000..7e006ef --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_required_args.php @@ -0,0 +1,20 @@ +createDlpJob('this/is/a/parent'); + +// required args string (double quotes) +$dlp->createDlpJob("this/is/a/$variable"); + +// required args inline array +$dlp->createDlpJob(['jobId' => 'abc', 'locationId' => 'def']); + +// required args variable +$dlp->createDlpJob($foo); diff --git a/examples/ClientUpgradeFixer/legacy_var_typehint.php b/examples/ClientUpgradeFixer/legacy_var_typehint.php new file mode 100644 index 0000000..e7143c2 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_var_typehint.php @@ -0,0 +1,60 @@ +listInfoTypes(); +$secrets = $secretmanager->listSecrets('this/is/a/parent'); + +// these shouldn't update +$operations = $longrunning->listOperations(); + +function get_dlp_service_client() +{ + return new DlpServiceClient(); +} + +function get_secretmanager_service_client() +{ + return new SecretManagerServiceClient(); +} + +function get_operations_service_client() +{ + return new DlpServiceClient(); +} + +class VariablesInsideClass extends TestCase +{ + /** @var DlpServiceClient $dlp */ + private $dlp; + private SecretManagerServiceClient $secretmanager; + + public function callDlp() + { + // These should update + $infoTypes = $this->dlp->listInfoTypes(); + $secrets = $this->secretmanager->listSecrets('this/is/a/parent'); + } +} diff --git a/examples/ClientUpgradeFixer/legacy_vars_defined_elsewhere.php b/examples/ClientUpgradeFixer/legacy_vars_defined_elsewhere.php new file mode 100644 index 0000000..d759d33 --- /dev/null +++ b/examples/ClientUpgradeFixer/legacy_vars_defined_elsewhere.php @@ -0,0 +1,31 @@ +listInfoTypes(); +// this should also update (from config) +$secrets = $secretmanager->listSecrets('this/is/a/parent'); + +// these shouldn't update +$operations = $longrunning->listOperations(); +$serviceAccount = $storage->getServiceAccount(); + +class MyClass extends SomethingWhichDefinedAClient +{ + public function callTheDlpClient() + { + $this->dlpClient->listInfoTypes(); + } + + public function callTheDlpClientStatic() + { + self::$dlpClient->listInfoTypes(); + } +} diff --git a/examples/ClientUpgradeFixer/new_class_vars.php b/examples/ClientUpgradeFixer/new_class_vars.php new file mode 100644 index 0000000..50ee864 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_class_vars.php @@ -0,0 +1,51 @@ +dlp = new DlpServiceClient(); + $this->secretmanager = new SecretManagerServiceClient(); + } + + public function callDlp() + { + $listInfoTypesRequest = new ListInfoTypesRequest(); + $infoTypes = $this->dlp->listInfoTypes($listInfoTypesRequest); + } + + public function callSecretManager() + { + $listSecretsRequest = (new ListSecretsRequest()) + ->setParent('this/is/a/parent'); + $secrets = $this->secretmanager->listSecrets($listSecretsRequest); + } +} + +// Instantiate a wrapping object. +$wrapper = new ClientWrapper(); + +// these should update +$listInfoTypesRequest2 = new ListInfoTypesRequest(); +$infoTypes = $wrapper->dlp->listInfoTypes($listInfoTypesRequest2); +$listSecretsRequest2 = (new ListSecretsRequest()) + ->setParent('this/is/a/parent'); +$secrets = $wrapper->secretmanager->listSecrets($listSecretsRequest2); diff --git a/examples/ClientUpgradeFixer/new_inline_html.php b/examples/ClientUpgradeFixer/new_inline_html.php new file mode 100644 index 0000000..0361126 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_inline_html.php @@ -0,0 +1,55 @@ +serializeToJsonString(), true), + JSON_PRETTY_PRINT + ); +} + +$listSecretsRequest = (new ListSecretsRequest()) + ->setParent($parent); +$listDlpJobsRequest = (new ListDlpJobsRequest()) + ->setParent($parent); +?> + + + + + +

Google Cloud Sample App

+
+

List Secrets

+
+ listSecrets($listSecretsRequest) as $secret): ?> +
+ +
+ +

List DLP Jobs

+
+ listDlpJobs($listDlpJobsRequest) as $job): ?> +
+ +
+
+ + diff --git a/examples/ClientUpgradeFixer/new_kitchen_sink.php b/examples/ClientUpgradeFixer/new_kitchen_sink.php new file mode 100644 index 0000000..5be74ac --- /dev/null +++ b/examples/ClientUpgradeFixer/new_kitchen_sink.php @@ -0,0 +1,92 @@ +listInfoTypes($listInfoTypesRequest); + +// optional args array (variable form) +$listInfoTypesRequest2 = new ListInfoTypesRequest(); +$dlp->listInfoTypes($listInfoTypesRequest2); + +// required args variable +$createDlpJobRequest = (new CreateDlpJobRequest()) + ->setParent($foo); +$dlp->createDlpJob($createDlpJobRequest); + +// required args string +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent('this/is/a/parent'); +$dlp->createDlpJob($createDlpJobRequest2); + +// required args array +$createDlpJobRequest3 = (new CreateDlpJobRequest()) + ->setParent(['jobId' => 'abc', 'locationId' => 'def']); +$dlp->createDlpJob($createDlpJobRequest3); + +// required args variable and optional args array +$createDlpJobRequest4 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setJobId('abc') + ->setLocationId('def'); +$dlp->createDlpJob($createDlpJobRequest4); + +// required args variable and optional args variable +$createDlpJobRequest5 = (new CreateDlpJobRequest()) + ->setParent($parent); +$dlp->createDlpJob($createDlpJobRequest5); + +// required args variable and optional args array with nested array +$createDlpJobRequest6 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setInspectJob(new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ])) + ->setTrailingComma(true); +$job = $dlp->createDlpJob($createDlpJobRequest6); + +$projectId = 'my-project'; +$secretId = 'my-secret'; + +// Create the Secret Manager client. +$client = new SecretManagerServiceClient(); + +// Build the parent name from the project. +$parent = $client->projectName($projectId); + +// Create the parent secret. +$createSecretRequest = (new CreateSecretRequest()) + ->setParent($parent) + ->setSecretId($secretId) + ->setSecret(new Secret([ + 'replication' => new Replication([ + 'automatic' => new Automatic(), + ]), + ])); +$secret = $client->createSecret($createSecretRequest); diff --git a/examples/ClientUpgradeFixer/new_mixins.php b/examples/ClientUpgradeFixer/new_mixins.php new file mode 100644 index 0000000..4b0348f --- /dev/null +++ b/examples/ClientUpgradeFixer/new_mixins.php @@ -0,0 +1,34 @@ +setResource($resource); +$policy = $secretManager->getIamPolicy($getIamPolicyRequest); + +// IAM conditions need at least version 3 +if ($policy->getVersion() != 3) { + $policy->setVersion(3); +} + +$binding = new Binding([ + 'role' => 'roles/spanner.fineGrainedAccessUser', + 'members' => [$iamMember], + 'condition' => new Expr([ + 'title' => $title, + 'expression' => sprintf("resource.name.endsWith('/databaseRoles/%s')", $databaseRole) + ]) +]); +$policy->setBindings([$binding]); +$setIamPolicyRequest = (new SetIamPolicyRequest()) + ->setResource($resource) + ->setPolicy($policy); +$secretManager->setIamPolicy($setIamPolicyRequest); diff --git a/examples/ClientUpgradeFixer/new_multiple_clients.php b/examples/ClientUpgradeFixer/new_multiple_clients.php new file mode 100644 index 0000000..f87b65a --- /dev/null +++ b/examples/ClientUpgradeFixer/new_multiple_clients.php @@ -0,0 +1,33 @@ +listInfoTypes($listInfoTypesRequest); +$listSecretsRequest = (new ListSecretsRequest()) + ->setParent('this/is/a/parent'); +$secrets = $secretmanager->listSecrets($listSecretsRequest); + +// these shouldn't update +$operations = $longrunning->listOperations(); +$serviceAccount = $storage->getServiceAccount(); diff --git a/examples/ClientUpgradeFixer/new_no_args.php b/examples/ClientUpgradeFixer/new_no_args.php new file mode 100644 index 0000000..021bafb --- /dev/null +++ b/examples/ClientUpgradeFixer/new_no_args.php @@ -0,0 +1,13 @@ +listInfoTypes($listInfoTypesRequest); diff --git a/examples/ClientUpgradeFixer/new_non_rpc_methods.php b/examples/ClientUpgradeFixer/new_non_rpc_methods.php new file mode 100644 index 0000000..72e37bf --- /dev/null +++ b/examples/ClientUpgradeFixer/new_non_rpc_methods.php @@ -0,0 +1,24 @@ +dlpJobName('my-project', 'my-job'); + +// Call the "close" method +$job = $dlp->close(); + +// Call an RPC method +$getDlpJobRequest = (new GetDlpJobRequest()) + ->setName($jobName); +$job = $dlp->getDlpJob($getDlpJobRequest); + +// Call a non-existant method! +$job = $dlp->getJob($jobName); diff --git a/examples/ClientUpgradeFixer/new_optional_args.php b/examples/ClientUpgradeFixer/new_optional_args.php new file mode 100644 index 0000000..d666599 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_optional_args.php @@ -0,0 +1,40 @@ +listInfoTypes($listInfoTypesRequest); + +// optional args array (inline array) +$createDlpJobRequest = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setJobId('abc') + ->setLocationId('def'); +$job = $dlp->createDlpJob($createDlpJobRequest); + +// optional args array (inline with nested arrays) +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setInspectJob(new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ])); +$job = $dlp->createDlpJob($createDlpJobRequest2); diff --git a/examples/ClientUpgradeFixer/new_optional_args_array_keyword.php b/examples/ClientUpgradeFixer/new_optional_args_array_keyword.php new file mode 100644 index 0000000..9ea8f2f --- /dev/null +++ b/examples/ClientUpgradeFixer/new_optional_args_array_keyword.php @@ -0,0 +1,40 @@ +listInfoTypes($listInfoTypesRequest); + +// optional args array (inline array) +$createDlpJobRequest = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setJobId('abc') + ->setLocationId('def'); +$job = $dlp->createDlpJob($createDlpJobRequest); + +// optional args array (inline with nested arrays) +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setInspectJob(new InspectJobConfig(array( + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ))); +$job = $dlp->createDlpJob($createDlpJobRequest2); diff --git a/examples/ClientUpgradeFixer/new_optional_args_variable.php b/examples/ClientUpgradeFixer/new_optional_args_variable.php new file mode 100644 index 0000000..6880d46 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_optional_args_variable.php @@ -0,0 +1,44 @@ +listInfoTypes($listInfoTypesRequest); + +// optional args array (inline array) +$options = ['jobId' => 'abc', 'locationId' => 'def']; +$createDlpJobRequest = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setJobId($options['jobId']) + ->setLocationId($options['locationId']); +$job = $dlp->createDlpJob($createDlpJobRequest); + +// optional args array (inline with nested arrays) +$options2 = [ + 'inspectJob' => new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ]) +]; +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setInspectJob($options2['inspectJob']); +$job = $dlp->createDlpJob($createDlpJobRequest2); diff --git a/examples/ClientUpgradeFixer/new_required_and_optional_args.php b/examples/ClientUpgradeFixer/new_required_and_optional_args.php new file mode 100644 index 0000000..72a7611 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_required_and_optional_args.php @@ -0,0 +1,47 @@ +setParent($parent); +$dlp->createDlpJob($createDlpJobRequest); + +// required args variable and optional args array +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setJobId('abc') + ->setLocationId('def'); +$dlp->createDlpJob($createDlpJobRequest2); + +// required args string and optional variable +$createDlpJobRequest3 = (new CreateDlpJobRequest()) + ->setParent('path/to/parent') + ->setJobId('abc') + ->setLocationId('def'); +$dlp->createDlpJob($createDlpJobRequest3); + +// required args variable and optional args array with nested array +$createDlpJobRequest4 = (new CreateDlpJobRequest()) + ->setParent($parent) + ->setInspectJob(new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) + ->setInfoTypes($infoTypes) + ->setIncludeQuote(true), + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), + ])); +$job = $dlp->createDlpJob($createDlpJobRequest4); diff --git a/examples/ClientUpgradeFixer/new_required_args.php b/examples/ClientUpgradeFixer/new_required_args.php new file mode 100644 index 0000000..f0730fe --- /dev/null +++ b/examples/ClientUpgradeFixer/new_required_args.php @@ -0,0 +1,29 @@ +setParent('this/is/a/parent'); +$dlp->createDlpJob($createDlpJobRequest); + +// required args string (double quotes) +$createDlpJobRequest2 = (new CreateDlpJobRequest()) + ->setParent("this/is/a/$variable"); +$dlp->createDlpJob($createDlpJobRequest2); + +// required args inline array +$createDlpJobRequest3 = (new CreateDlpJobRequest()) + ->setParent(['jobId' => 'abc', 'locationId' => 'def']); +$dlp->createDlpJob($createDlpJobRequest3); + +// required args variable +$createDlpJobRequest4 = (new CreateDlpJobRequest()) + ->setParent($foo); +$dlp->createDlpJob($createDlpJobRequest4); diff --git a/examples/ClientUpgradeFixer/new_var_typehint.php b/examples/ClientUpgradeFixer/new_var_typehint.php new file mode 100644 index 0000000..6eaa573 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_var_typehint.php @@ -0,0 +1,65 @@ +listInfoTypes($listInfoTypesRequest); +$listSecretsRequest = (new ListSecretsRequest()) + ->setParent('this/is/a/parent'); +$secrets = $secretmanager->listSecrets($listSecretsRequest); + +// these shouldn't update +$operations = $longrunning->listOperations(); + +function get_dlp_service_client() +{ + return new DlpServiceClient(); +} + +function get_secretmanager_service_client() +{ + return new SecretManagerServiceClient(); +} + +function get_operations_service_client() +{ + return new DlpServiceClient(); +} + +class VariablesInsideClass extends TestCase +{ + /** @var DlpServiceClient $dlp */ + private $dlp; + private SecretManagerServiceClient $secretmanager; + + public function callDlp() + { + // These should update + $infoTypes = $this->dlp->listInfoTypes(); + $secrets = $this->secretmanager->listSecrets('this/is/a/parent'); + } +} diff --git a/examples/ClientUpgradeFixer/new_vars_defined_elsewhere.php b/examples/ClientUpgradeFixer/new_vars_defined_elsewhere.php new file mode 100644 index 0000000..688d661 --- /dev/null +++ b/examples/ClientUpgradeFixer/new_vars_defined_elsewhere.php @@ -0,0 +1,38 @@ +listInfoTypes($listInfoTypesRequest); +// this should also update (from config) +$listSecretsRequest = (new ListSecretsRequest()) + ->setParent('this/is/a/parent'); +$secrets = $secretmanager->listSecrets($listSecretsRequest); + +// these shouldn't update +$operations = $longrunning->listOperations(); +$serviceAccount = $storage->getServiceAccount(); + +class MyClass extends SomethingWhichDefinedAClient +{ + public function callTheDlpClient() + { + $listInfoTypesRequest2 = new ListInfoTypesRequest(); + $this->dlpClient->listInfoTypes($listInfoTypesRequest2); + } + + public function callTheDlpClientStatic() + { + $listInfoTypesRequest3 = new ListInfoTypesRequest(); + self::$dlpClient->listInfoTypes($listInfoTypesRequest3); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0ef71a6..be9f1fb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,7 @@ test/Utils test/TestUtils + test/Fixers diff --git a/src/Fixers/ClientUpgradeFixer/ClientUpgradeFixer.php b/src/Fixers/ClientUpgradeFixer/ClientUpgradeFixer.php new file mode 100644 index 0000000..7adf779 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/ClientUpgradeFixer.php @@ -0,0 +1,257 @@ + $configuration + */ + public function configure(array $configuration): void + { + // no configuration assumes true + $this->configuration = $configuration; + } + + /** + * Defines the available configuration options of the fixer. + */ + public function getConfigurationDefinition(): FixerConfigurationResolverInterface + { + return new FixerConfigurationResolver([ + (new FixerOptionBuilder('clientVars', 'A map of client variables to their new class names')) + ->setAllowedTypes(['array']) + ->setDefault([]) + ->getOption(), + ]); + } + + protected function applyFix(\SplFileInfo $file, Tokens $tokens): void + { + if (!class_exists('Google\Auth\OAuth2')) { + throw new \LogicException( + 'In order for Google\Cloud\NewSurfaceFixer to work, you must install the google ' + . 'cloud client library and include its autoloader in .php-cs-fixer.dist.php' + ); + } + + $clients = []; + $useDeclarations = UseStatement::getUseDeclarations($tokens); + foreach (UseStatement::getImportedClients($useDeclarations) as $clientClass => $useDeclaration) { + $newClientName = ClientVar::getNewClassFromClassname($clientClass); + if (class_exists($newClientName)) { + // Rename old clients to new namespaces + $tokens->overrideRange( + $useDeclaration->getStartIndex(), + $useDeclaration->getEndIndex(), + UseStatement::getTokensFromClassName($newClientName) + ); + $clients[] = $clientClass; + } + } + + // Get variable names for all clients + $clientShortNames = []; + foreach ($clients as $clientClass) { + // Save the client shortnames so we can search for them below + $parts = explode('\\', $clientClass); + $shortName = array_pop($parts); + $clientShortNames[$clientClass] = $shortName; + } + $clientVars = array_merge( + ClientVar::getClientVarsFromNewKeyword($tokens, $clientShortNames), + ClientVar::getClientVarsFromVarTypehint($tokens, $clientShortNames), + ClientVar::getClientVarsFromConfiguration($this->configuration), + ); + + // Find the RPC methods being called on the clients + $classesToImport = []; + $counter = new RequestVariableCounter(); + $importStart = $this->getImportStart($tokens); + $insertStart = null; + for ($index = 0; $index < count($tokens); $index++) { + $clientVar = $clientVars[$tokens[$index]->getContent()] ?? null; + if (is_null($clientVar)) { + // The token does not contain a client var + continue; + } + + if (!$clientVar->isDeclaredAt($tokens, $index)) { + // The token looks like our client var but isn't + continue; + } + + $operatorIndex = $tokens->getNextMeaningfulToken($index); + if (!$tokens[$operatorIndex]->isGivenKind(T_OBJECT_OPERATOR)) { + // The client var is not calling a method + continue; + } + + // The method being called by the client variable + $methodIndex = $tokens->getNextMeaningfulToken($operatorIndex); + if (!$rpcMethod = $clientVar->getRpcMethod($tokens[$methodIndex]->getContent())) { + // The method doesn't exist, or is not an RPC call + continue; + } + + // Get the arguments being passed to the RPC method + [$arguments, $firstIndex, $lastIndex] = RpcParameter::getRpcCallParameters($tokens, $methodIndex); + + // determine where to insert the new tokens + $lineStart = $clientVar->getLineStart($tokens); + + // Handle differently when we are dealing with inline PHP + $isInlinePhpCall = $tokens[$lineStart]->getId() === T_OPEN_TAG; + + $indent = ''; + if (!$isInlinePhpCall) { + $indent = str_replace("\n", '', $tokens[$lineStart]->getContent()); + } + + $requestClass = $rpcMethod->getRequestClass(); + $requestVarName = $counter->getNextVariableName($requestClass->getShortName()); + + // Tokens for the setters called on the new request object + $requestSetterTokens = $rpcMethod->getRequestSetterTokens($tokens, $arguments, $indent); + + // Tokens for initializing the new request variable + $newRequestTokens = $requestClass->getInitTokens( + $requestVarName, + count($requestSetterTokens) > 0 + ); + + // Add them together + $newRequestTokens = array_merge( + [new Token([T_WHITESPACE, PHP_EOL . $indent])], + $newRequestTokens, + $requestSetterTokens, + [new Token(';')] + ); + + // When inserting for inline PHP, add a newline before the first request variable + if ($isInlinePhpCall && $counter->isFirstVar()) { + array_unshift($newRequestTokens, new Token([T_WHITESPACE, PHP_EOL])); + } + + // Determine where the request variable tokens should be inserted + if ($isInlinePhpCall) { + // If we are inline, insert right before the first closing PHP tag + if (is_null($insertStart)) { + $insertStart = $tokens->getNextTokenOfKind($importStart, ['?>', [T_CLOSE_TAG]]) - 1; + } + } else { + // else, insert at beginning of the line of the original RPC call + $insertStart = $lineStart; + } + + // insert the request variable tokens + $tokens->insertAt($insertStart, $newRequestTokens); + + // Replace the original RPC call arguments with the new request variable + $tokens->overrideRange( + $firstIndex + 1 + count($newRequestTokens), + $lastIndex - 1 + count($newRequestTokens), + [new Token([T_VARIABLE, $requestVarName])] + ); + + // Increment the current $index and $insertStart + $index = $firstIndex + 1 + count($newRequestTokens); + if ($isInlinePhpCall) { + $insertStart = $insertStart + count($newRequestTokens); + } + + // Add the request class to be imported later + $classesToImport[$requestClass->getName()] = $requestClass; + } + + // Import the new request classes + if ($classesToImport) { + $importedClasses = array_map(fn ($useDeclaration) => $useDeclaration->getFullName(), $useDeclarations); + $classesToImport = array_filter( + $classesToImport, + fn($requestClass) => !isset($importedClasses[$requestClass->getName()]) + ); + $requestClassImportTokens = array_map( + fn($requestClass) => $requestClass->getImportTokens(), + array_values($classesToImport) + ); + $tokens->insertAt($importStart, array_merge(...$requestClassImportTokens)); + // Ensure new imports are in the correct order + $orderFixer = new OrderedImportsFixer(); + $orderFixer->fix($file, $tokens); + } + } + + private function getImportStart(Tokens $tokens) + { + $useDeclarations = UseStatement::getUseDeclarations($tokens); + if (count($useDeclarations) > 0) { + return $useDeclarations[count($useDeclarations) - 1]->getEndIndex() + 1; + } + + // There will be no changes made if there are no imports, so this logic + // should not matter + + return $tokens->getNextMeaningfulToken(0); + } + + /** + * Returns the definition of the fixer. + */ + public function getDefinition(): FixerDefinitionInterface + { + return new FixerDefinition('Upgrade code to the new Google Cloud PHP client surface', []); + } + + /** + * Returns the name of the fixer. + * + * The name must be all lowercase and without any spaces. + * + * @return string The name of the fixer + */ + public function getName(): string + { + return 'GoogleCloud/upgrade_clients'; + } + + /** + * {@inheritdoc} + * + * Must run before OrderedImportsFixer. + */ + public function getPriority(): int + { + return 0; + } +} diff --git a/src/Fixers/ClientUpgradeFixer/ClientVar.php b/src/Fixers/ClientUpgradeFixer/ClientVar.php new file mode 100644 index 0000000..24051cc --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/ClientVar.php @@ -0,0 +1,180 @@ +varName = $varName; + $this->className = $className; + } + + public function getClassName(): string + { + return $this->className; + } + + /** + * @param Tokens $tokens + * @param int $index + * @return bool + */ + public function isDeclaredAt(Tokens $tokens, int $index): bool + { + $token = $tokens[$index]; + if ($token->isGivenKind(T_VARIABLE) + || ($token->isGivenKind(T_STRING) && $tokens[$index-1]->isGivenKind(T_OBJECT_OPERATOR)) + ) { + $this->startIndex = $index; + return true; + } + return false; + } + + public function getNewClassName(): string + { + return static::getNewClassFromClassname($this->className); + } + + public static function getNewClassFromClassname(string $className): string + { + $parts = explode('\\', $className); + $shortName = array_pop($parts); + return implode('\\', $parts) . '\\Client\\' . $shortName; + } + + public function getRpcMethod(string $rpcName): ?RpcMethod + { + // Get the Request class name + $newClientClass = $this->getNewClassname(); + if (!method_exists($newClientClass, $rpcName)) { + // If the new method doesn't exist, there's nothing we can do + return null; + } + $method = new ReflectionMethod($newClientClass, $rpcName); + $parameters = $method->getParameters(); + if (!isset($parameters[0]) || !$type = $parameters[0]->getType()) { + return null; + } + if ($type->isBuiltin()) { + // If the first parameter is a primitive type, assume this is a helper method + return null; + } + + return new RpcMethod($this, $rpcName); + } + + public function getLineStart(Tokens $tokens): int + { + // determine the indent + $indent = ''; + $lineStart = $this->startIndex; + $i = 1; + while ( + $this->startIndex - $i >= 0 + && false === strpos($tokens[$this->startIndex - $i]->getContent(), "\n") + && $tokens[$this->startIndex - $i]->getId() !== T_OPEN_TAG + ) { + $i++; + } + + return $this->startIndex - $i; + } + + public static function getClientVarsFromNewKeyword(Tokens $tokens, array $clientShortNames): array + { + $clientVars = []; + foreach ($tokens as $index => $token) { + // get variables which are set directly + if (!$token->isGivenKind(T_NEW)) { + continue; + } + + $nextToken = $tokens[$tokens->getNextMeaningfulToken($index)]; + $shortName = $nextToken->getContent(); + if (!in_array($shortName, $clientShortNames)) { + continue; + } + + if (!$prevIndex = $tokens->getPrevMeaningfulToken($index)) { + continue; + } + + if ($tokens[$prevIndex]->getContent() !== '=') { + continue; + } + + if (!$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex)) { + continue; + } + + if ( + $tokens[$prevIndex]->isGivenKind(T_VARIABLE) || ( + $tokens[$prevIndex]->isGivenKind(T_STRING) + && $tokens[$prevIndex-1]->isGivenKind(T_OBJECT_OPERATOR) + ) + ) { + // Handle clients set to $var + $clientClass = array_search($shortName, $clientShortNames); + $varName = $tokens[$prevIndex]->getContent(); + $clientVars[$varName] = new ClientVar($varName, $clientClass); + } + } + + return $clientVars; + } + + public static function getClientVarsFromVarTypehint(Tokens $tokens, array $clientShortNames): array + { + $clientVars = []; + foreach ($tokens as $index => $token) { + // get variables which are set directly + if (!$token->isGivenKind(T_DOC_COMMENT)) { + continue; + } + + if (false === strpos($token->getContent(), '@var')) { + continue; + } + + $varToken = $tokens[$tokens->getNextMeaningfulToken($index)]; + if (!$varToken->isGivenKind(T_VARIABLE)) { + continue; + } + + $regex = sprintf('/@var (.*) \\%s/', $varToken->getContent()); + if (!preg_match($regex, $token->getContent(), $matches)) { + continue; + } + + $shortName = $matches[1]; + $varName = $varToken->getContent(); + if ($clientClass = array_search($shortName, $clientShortNames)) { + $clientVars[$varName] = new ClientVar($varName, $clientClass); + } + } + + return $clientVars; + } + + public static function getClientVarsFromConfiguration(array $configuration): array + { + $clientVars = []; + if (isset($configuration['clientVars'])) { + foreach ($configuration['clientVars'] as $varName => $clientClass) { + $clientVars[$varName] = new ClientVar($varName, $clientClass); + } + } + return $clientVars; + } +} diff --git a/src/Fixers/ClientUpgradeFixer/README.md b/src/Fixers/ClientUpgradeFixer/README.md new file mode 100644 index 0000000..c293693 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/README.md @@ -0,0 +1,114 @@ +# Fixer for the new Google Cloud PHP Client Surface + +This repo provides a Fixer, to be used with [PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer), +which will automatically** upgrade your code to the +[new Google Cloud PHP client surface](https://github.com/googleapis/google-cloud-php/discussions/5206). + +** this is an alpha tool and NOT recommended to use without thorough testing! +It is also not guanteed by Google in any way. + +## Installation + + +Install the `google/cloud-tools` package, which includes the fixer: + +```sh +composer require --dev "google/cloud-tools" +``` + +Install `friendsofphp/php-cs-fixer`: + +```sh +composer require --dev "friendsofphp/php-cs-fixer:^3.21" +``` + +## Running the fixer + +First, create a `.php-cs-fixer.google.php` in your project which will be +configured to use the custom fixer: + +```php +registerCustomFixers([ + new Google\Cloud\Fixers\ClientUpgradeFixer\ClientUpgradeFixer(), + ]) + ->setRules([ + 'GoogleCloud/upgrade_clients' => true, + ]) +; +``` + +Next run this fixer with the following command: + +```sh +# use the examples provided in this repo +export DIR=vendor/google/cloud-tools/examples/ClientUpgradeFixer + +# run the CS fixer for that directory using the config above +vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.google.php --dry-run --diff $DIR +``` + +You should get an output similar to this + +```diff +--- legacy_optional_args.php ++++ legacy_optional_args.php +@@ -2,10 +2,12 @@ + + namespace Google\Cloud\Samples\Dlp; + +-use Google\Cloud\Dlp\V2\DlpServiceClient; ++use Google\Cloud\Dlp\V2\Client\DlpServiceClient; ++use Google\Cloud\Dlp\V2\CreateDlpJobRequest; + use Google\Cloud\Dlp\V2\InspectConfig; + use Google\Cloud\Dlp\V2\InspectJobConfig; + use Google\Cloud\Dlp\V2\Likelihood; ++use Google\Cloud\Dlp\V2\ListInfoTypesRequest; + use Google\Cloud\Dlp\V2\StorageConfig; + + // Instantiate a client. +@@ -12,14 +14,20 @@ + $dlp = new DlpServiceClient(); + + // optional args array (variable) +-$infoTypes = $dlp->listInfoTypes($parent); ++$request = (new ListInfoTypesRequest()); ++$infoTypes = $dlp->listInfoTypes($request); + + // optional args array (inline array) +-$job = $dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']); ++$request2 = (new CreateDlpJobRequest()) ++ ->setParent($parent) ++ ->setJobId('abc') ++ ->setLocationId('def'); ++$job = $dlp->createDlpJob($request2); + + // optional args array (inline with nested arrays) +-$job = $dlp->createDlpJob($parent, [ +- 'inspectJob' => new InspectJobConfig([ ++$request3 = (new CreateDlpJobRequest()) ++ ->setParent($parent) ++ ->setInspectJob(new InspectJobConfig([ + 'inspect_config' => (new InspectConfig()) + ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED) + ->setLimits($limits) +@@ -28,5 +36,5 @@ + 'storage_config' => (new StorageConfig()) + ->setCloudStorageOptions(($cloudStorageOptions)) + ->setTimespanConfig($timespanConfig), +- ]) +-]); ++ ])); ++$job = $dlp->createDlpJob($request3); + + ----------- end diff ----------- +``` diff --git a/src/Fixers/ClientUpgradeFixer/RequestClass.php b/src/Fixers/ClientUpgradeFixer/RequestClass.php new file mode 100644 index 0000000..78ca769 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/RequestClass.php @@ -0,0 +1,53 @@ +reflection = new ReflectionClass($className); + } + + public function getShortName(): string + { + return $this->reflection->getShortName(); + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getImportTokens(): array + { + return array_merge( + [new Token([T_WHITESPACE, PHP_EOL])], + UseStatement::getTokensFromClassName($this->getName()) + ); + } + + public function getInitTokens(string $requestVarName, bool $parenthesis) + { + // Add the code for creating the $request variable + return array_filter([ + new Token([T_VARIABLE, $requestVarName]), + new Token([T_WHITESPACE, ' ']), + new Token('='), + new Token([T_WHITESPACE, ' ']), + $parenthesis ? new Token('(') : null, + new Token([T_NEW, 'new']), + new Token([T_WHITESPACE, ' ']), + new Token([T_STRING, $this->getShortName()]), + new Token('('), + new Token(')'), + $parenthesis ? new Token(')') : null, + ]); + } +} diff --git a/src/Fixers/ClientUpgradeFixer/RequestVariableCounter.php b/src/Fixers/ClientUpgradeFixer/RequestVariableCounter.php new file mode 100644 index 0000000..6f7dcc5 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/RequestVariableCounter.php @@ -0,0 +1,30 @@ +varCounts) == 1 + && array_values($this->varCounts)[0] == 1; + } + + public function getNextVariableName(string $shortName): string + { + if (!isset($this->varCounts[$shortName])) { + $this->varCounts[$shortName] = 0; + } + $num = (string) ++$this->varCounts[$shortName]; + // determine $request variable name depending on call count + return sprintf( + '$%s%s', + lcfirst($shortName), + $num == '1' ? '' : $num + ); + } +} diff --git a/src/Fixers/ClientUpgradeFixer/RpcMethod.php b/src/Fixers/ClientUpgradeFixer/RpcMethod.php new file mode 100644 index 0000000..9f3b109 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/RpcMethod.php @@ -0,0 +1,222 @@ +legacyReflection = new ReflectionMethod($clientVar->getClassName(), $methodName); + $this->newReflection = new ReflectionMethod($clientVar->getNewClassName(), $methodName); + } + + public function getRequestClass(): RequestClass + { + $firstParameter = $this->newReflection->getParameters()[0]; + return new RequestClass($firstParameter->getType()->getName()); + } + + public function getRequestSetterTokens(Tokens $tokens, array $arguments, string $indent) + { + $argIndex = 0; + $requestSetterTokens = []; + foreach ($arguments as $startIndex => $argumentTokens) { + $setters = $this->getSettersFromTokens($tokens, $startIndex, $argIndex, $argumentTokens); + foreach ($setters as $setter) { + $requestSetterTokens = array_merge( + $requestSetterTokens, + $this->getTokensForSetter($setter, $indent) + ); + } + $argIndex++; + } + return $requestSetterTokens; + } + + private function getSettersFromTokens( + Tokens $tokens, + int $startIndex, + int $argIndex, + array $argumentTokens + ): array { + if ($rpcParameter = $this->getParameterAtIndex($argIndex)) { + // handle array of optional args! + if ($rpcParameter->isOptionalArgs()) { + $argumentStart = $tokens->getNextMeaningfulToken($startIndex); + if ($tokens[$argumentStart]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN) + || $tokens[$argumentStart]->isGivenKind(T_ARRAY)) { + // If the array is being passed directly to the RPC method + return $this->settersFromArgumentArray($tokens, $argumentStart); + } + + if ($tokens[$argumentStart]->isGivenKind(T_VARIABLE)) { + // if a variable is being passed in, find where the variable is defined + $optionalArgsVar = $tokens[$argumentStart]->getContent(); + for ($index = $argumentStart - 1; $index > 0; $index--) { + $token = $tokens[$index]; + // Find where the optionalArgs variable is defined + if ($token->isGivenKind(T_VARIABLE) && $token->getContent() == $optionalArgsVar) { + $nextIndex = $tokens->getNextMeaningfulToken($index); + if ($tokens[$nextIndex]->getContent() == '=') { + $argumentStart = $tokens->getNextMeaningfulToken($nextIndex); + return $this->settersFromOptionalArgsVar($tokens, $argumentStart, $optionalArgsVar); + } + } + } + } + } else { + // Just place the argument tokens in a setter + $setterName = $rpcParameter->getSetter(); + // Remove leading whitespace + for ($i = 0; $argumentTokens[$i]->isGivenKind(T_WHITESPACE); $i++) { + unset($argumentTokens[$i]); + } + return [[$setterName, $argumentTokens]]; + } + } else { + // Could not find the argument for $clientFullName and $rpcName at index $argIndex + } + return []; + } + + private function getParameterAtIndex(int $index): ?RpcParameter + { + $params = $this->legacyReflection->getParameters(); + if (isset($params[$index])) { + return new RpcParameter($params[$index]); + } + + return null; + } + + private function getSetterIndiciesFromInlineArray(Tokens $tokens, int $index, int $closeIndex) + { + $arrayEntries = $tokens->findGivenKind(T_DOUBLE_ARROW, $index, $closeIndex); + $nestedArrays = $tokens->findGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN, $index + 1, $closeIndex); + $nestedLegacyArrays = $tokens->findGivenKind(T_ARRAY, $index + 1, $closeIndex); + + // skip nested arrays + foreach ($arrayEntries as $doubleArrowIndex => $doubleArrowIndexToken) { + foreach ($nestedArrays as $nestedArrayIndex => $nestedArrayIndexToken) { + $nestedArrayCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $nestedArrayIndex); + if ($doubleArrowIndex > $nestedArrayIndex && $doubleArrowIndex < $nestedArrayCloseIndex) { + unset($arrayEntries[$doubleArrowIndex]); + } + } + foreach ($nestedLegacyArrays as $nestedLegacyArrayIndex => $nestedLegacyArrayIndexToken) { + $nestedLegacyArrayCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nestedLegacyArrayIndex + 1); + if ($doubleArrowIndex > $nestedLegacyArrayIndex && $doubleArrowIndex < $nestedLegacyArrayCloseIndex) { + unset($arrayEntries[$doubleArrowIndex]); + } + } + } + + return array_keys($arrayEntries); + } + + private function getTokensForSetter(array $setter, string $indent): array + { + list($method, $varTokens) = $setter; + + $tokens = [ + // whitespace (assume 4 spaces) + new Token([T_WHITESPACE, PHP_EOL . $indent . ' ']), + // setter method + new Token([T_OBJECT_OPERATOR, '->']), + new Token([T_STRING, $method]), + // setter value + new Token('('), + ]; + // merge in var tokens + $tokens = array_merge($tokens, $varTokens); + + // add closing parenthesis + $tokens[] = new Token(')'); + + return $tokens; + } + + private function settersFromArgumentArray(Tokens $tokens, int $index): array + { + $setters = []; + $legacyArraySyntax = $tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN); + $closeIndex = $legacyArraySyntax + ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index) + : $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, ++$index); + + $arrayEntryIndices = $this->getSetterIndiciesFromInlineArray($tokens, $index, $closeIndex, $legacyArraySyntax); + + foreach ($arrayEntryIndices as $i => $doubleArrowIndex) { + $keyIndex = $tokens->getNextMeaningfulToken($index); + if (!$tokens[$keyIndex]->isGivenKind(T_CONSTANT_ENCAPSED_STRING)) { + continue; + } + $setterName = 'set' . ucfirst(trim($tokens[$keyIndex]->getContent(), '"\'')); + $tokens->removeLeadingWhitespace($doubleArrowIndex + 1); + $valueEnd = isset($arrayEntryIndices[$i+1]) + ? $tokens->getPrevTokenOfKind($arrayEntryIndices[$i+1], [new Token(',')]) + : $closeIndex; + $varTokens = array_slice($tokens->toArray(), $doubleArrowIndex + 1, $valueEnd - $doubleArrowIndex - 1); + // Remove trailing whitespace + for ($i = count($varTokens)-1; $varTokens[$i]->isGivenKind(T_WHITESPACE); $i--) { + unset($varTokens[$i]); + } + // Remove trailing commas + for ($i = count($varTokens)-1; $varTokens[$i]->getContent() === ','; $i--) { + unset($varTokens[$i]); + } + // Remove leading whitespace + for ($i = 0; $varTokens[$i]->isGivenKind(T_WHITESPACE); $i++) { + unset($varTokens[$i]); + } + $setters[] = [$setterName, $varTokens]; + $index = $valueEnd; + } + return $setters; + } + + private function settersFromOptionalArgsVar(Tokens $tokens, int $index, string $optionalArgsVar): array + { + $setters = []; + if (!$tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN) + && !$tokens[$index]->isGivenKind(T_ARRAY)) { + return $setters; + } + + $closeIndex = $tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN) + ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index) + : $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index + 1); + $arrayEntryIndices = $this->getSetterIndiciesFromInlineArray($tokens, $index, $closeIndex); + + foreach ($arrayEntryIndices as $i => $doubleArrowIndex) { + $keyIndex = $tokens->getNextMeaningfulToken($index); + if (!$tokens[$keyIndex]->isGivenKind(T_CONSTANT_ENCAPSED_STRING)) { + continue; + } + + $setterName = 'set' . ucfirst(trim($tokens[$keyIndex]->getContent(), '"\'')); + $varTokens = [ + new Token([T_VARIABLE, $optionalArgsVar]), + new Token([CT::T_ARRAY_SQUARE_BRACE_OPEN, '[']), + clone $tokens[$keyIndex], + new Token([CT::T_ARRAY_SQUARE_BRACE_CLOSE, ']']), + ]; + $setters[] = [$setterName, $varTokens]; + $valueEnd = isset($arrayEntryIndices[$i+1]) + ? $tokens->getPrevTokenOfKind($arrayEntryIndices[$i+1], [new Token(',')]) + : $closeIndex; + $index = $valueEnd; + } + + return $setters; + } +} diff --git a/src/Fixers/ClientUpgradeFixer/RpcParameter.php b/src/Fixers/ClientUpgradeFixer/RpcParameter.php new file mode 100644 index 0000000..42dc526 --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/RpcParameter.php @@ -0,0 +1,101 @@ +reflection = $reflection; + } + + public function isOptionalArgs(): bool + { + return $this->reflection->getName() === 'optionalArgs'; + } + + public function getSetter(): string + { + return 'set' . ucfirst($this->reflection->getName()); + } + + public static function getRpcCallParameters(Tokens $tokens, int $startIndex) + { + $arguments = []; + $nextIndex = $tokens->getNextMeaningfulToken($startIndex); + $lastIndex = null; + if ($tokens[$nextIndex]->getContent() == '(') { + $startIndex = $nextIndex; + $lastIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex); + $nextArgumentEnd = self::getNextArgumentEnd($tokens, $nextIndex); + while ($nextArgumentEnd != $nextIndex) { + $argumentTokens = []; + for ($i = $nextIndex + 1; $i <= $nextArgumentEnd; $i++) { + $argumentTokens[] = $tokens[$i]; + } + + $arguments[$nextIndex] = $argumentTokens; + $nextIndex = $tokens->getNextMeaningfulToken($nextArgumentEnd); + $nextArgumentEnd = self::getNextArgumentEnd($tokens, $nextIndex); + } + } + + return [$arguments, $startIndex, $lastIndex]; + } + + private static function getNextArgumentEnd(Tokens $tokens, int $index): int + { + $nextIndex = $tokens->getNextMeaningfulToken($index); + $nextToken = $tokens[$nextIndex]; + + while ($nextToken->equalsAny([ + '$', + '[', + '(', + [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN], + [CT::T_ARRAY_SQUARE_BRACE_OPEN], + [CT::T_DYNAMIC_PROP_BRACE_OPEN], + [CT::T_DYNAMIC_VAR_BRACE_OPEN], + [CT::T_NAMESPACE_OPERATOR], + [T_NS_SEPARATOR], + [T_STATIC], + [T_STRING], + [T_CONSTANT_ENCAPSED_STRING], + [T_VARIABLE], + [T_NEW], + [T_ARRAY], + ])) { + $blockType = Tokens::detectBlockType($nextToken); + + if (null !== $blockType) { + $nextIndex = $tokens->findBlockEnd($blockType['type'], $nextIndex); + } + + $index = $nextIndex; + $nextIndex = $tokens->getNextMeaningfulToken($nextIndex); + $nextToken = $tokens[$nextIndex]; + } + + if ($nextToken->isGivenKind(T_OBJECT_OPERATOR)) { + return self::getNextArgumentEnd($tokens, $nextIndex); + } + + if ($nextToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) { + return self::getNextArgumentEnd($tokens, $tokens->getNextMeaningfulToken($nextIndex)); + } + + if ('"' === $nextToken->getContent()) { + if ($endIndex = $tokens->getNextTokenOfKind($nextIndex + 1, ['"'])) { + return $endIndex; + } + } + + return $index; + } +} diff --git a/src/Fixers/ClientUpgradeFixer/UseStatement.php b/src/Fixers/ClientUpgradeFixer/UseStatement.php new file mode 100644 index 0000000..61a644e --- /dev/null +++ b/src/Fixers/ClientUpgradeFixer/UseStatement.php @@ -0,0 +1,50 @@ +getFullName(); + $clientShortName = $useDeclaration->getShortName(); + if ( + 0 === strpos($clientClass, 'Google\\') + && 'Client' === substr($clientShortName, -6) + && false === strpos($clientClass, '\\Client\\') + && class_exists($clientClass) + ) { + if (false !== strpos(get_parent_class($clientClass), '\Gapic\\')) { + $clients[$clientClass] = $useDeclaration; + } + } + } + return $clients; + } + + public static function getUseDeclarations(Tokens $tokens): array + { + return (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens); + } +} diff --git a/test/Fixers/ClientUpgradeFixerTest.php b/test/Fixers/ClientUpgradeFixerTest.php new file mode 100644 index 0000000..ca06462 --- /dev/null +++ b/test/Fixers/ClientUpgradeFixerTest.php @@ -0,0 +1,61 @@ +fixer = new ClientUpgradeFixer(); + $this->fixer->configure([ + 'clientVars' => [ + '$secretmanager' => 'Google\\Cloud\\SecretManager\\V1\\SecretManagerServiceClient', + 'dlpClient' => 'Google\\Cloud\\Dlp\\V2\\DlpServiceClient', + '$dlpClient' => 'Google\\Cloud\\Dlp\\V2\\DlpServiceClient', + ] + ]); + } + + /** + * @dataProvider provideLegacySamples + */ + public function testLegacySamples($filename) + { + $legacyFilepath = self::SAMPLES_DIR . $filename; + $newFilepath = str_replace('legacy_', 'new_', $legacyFilepath); + $tokens = Tokens::fromCode(file_get_contents($legacyFilepath)); + $fileInfo = new SplFileInfo($legacyFilepath); + $this->fixer->fix($fileInfo, $tokens); + $code = $tokens->generateCode(); + if (!file_exists($newFilepath) || file_get_contents($newFilepath) !== $code) { + if (getenv('UPDATE_FIXTURES=1')) { + file_put_contents($newFilepath, $code); + $this->markTestIncomplete('Updated fixtures'); + } + if (!file_exists($newFilepath)) { + $this->fail('File does not exist'); + } + } + $this->assertStringEqualsFile($newFilepath, $code); + } + + public static function provideLegacySamples() + { + return array_map( + fn ($file) => [basename($file)], + array_filter( + glob(self::SAMPLES_DIR . '*'), + fn ($file) => 0 === strpos(basename($file), 'legacy_') + ) + ); + } +}