diff --git a/doc/user/project/repository/code_suggestions/repository_xray.md b/doc/user/project/repository/code_suggestions/repository_xray.md index 596bb7faef90d42606378da26d2db8b67360b092..e593fe12e15ce8a170f11d8e2eb9f690a0d0f90b 100644 --- a/doc/user/project/repository/code_suggestions/repository_xray.md +++ b/doc/user/project/repository/code_suggestions/repository_xray.md @@ -47,7 +47,7 @@ The Repository X-Ray searches a maximum of two directory levels from the reposit | Java | Gradle | `build.gradle` | 17.4 or later | | Java | Maven | `pom.xml` | 17.4 or later | | Kotlin | Gradle | `build.gradle.kts` | 17.5 or later | -| PHP | Composer | `composer.lock` | 17.5 or later | +| PHP | Composer | `composer.lock`, `composer.json` | 17.5 or later | | Python | Conda | `environment.yml` | 17.5 or later | | Python | Pip | `requirements.txt` | 17.5 or later | | Python | Poetry | `poetry.lock`, `pyproject.toml` | 17.5 or later | diff --git a/ee/lib/ai/context/dependencies/config_files/constants.rb b/ee/lib/ai/context/dependencies/config_files/constants.rb index 506c97cb6e65af33af3001b40a01feee070b1bb8..5b1b8e73561efd2e1a717db019dc986bb07cc72a 100644 --- a/ee/lib/ai/context/dependencies/config_files/constants.rb +++ b/ee/lib/ai/context/dependencies/config_files/constants.rb @@ -28,10 +28,11 @@ module Constants ConfigFiles::JavaMaven, ConfigFiles::KotlinGradle, ConfigFiles::PhpComposerLock, + ConfigFiles::PhpComposer, ConfigFiles::PythonConda, ConfigFiles::PythonPip, - ConfigFiles::PythonPoetry, ConfigFiles::PythonPoetryLock, + ConfigFiles::PythonPoetry, ConfigFiles::RubyGemsLock ].freeze end diff --git a/ee/lib/ai/context/dependencies/config_files/php_composer.rb b/ee/lib/ai/context/dependencies/config_files/php_composer.rb new file mode 100644 index 0000000000000000000000000000000000000000..de86341bf8b8048e86e75f2b629da61916bea314 --- /dev/null +++ b/ee/lib/ai/context/dependencies/config_files/php_composer.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Ai + module Context + module Dependencies + module ConfigFiles + class PhpComposer < Base + def self.file_name_glob + 'composer.json' + end + + def self.lang_name + 'PHP' + end + + private + + ### Example format: + # + # { + # { ... }, + # "require": { + # "php": "^7.2.5 || ^8.0", + # "composer/ca-bundle": "^1.5" + # }, + # "require-dev": { + # "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1", + # "phpstan/phpstan": "^1.11.8" + # }, + # { ... } + # } + # + def extract_libs + parsed = ::Gitlab::Json.parse(content) + + %w[require require-dev].flat_map do |key| + dig_in(parsed, key).try(:map) do |name, version| + Lib.new(name: name, version: version) + end + end.compact + rescue JSON::ParserError + raise ParsingError, 'content is not valid JSON' + end + end + end + end + end +end diff --git a/ee/spec/lib/ai/context/dependencies/config_files/php_composer_spec.rb b/ee/spec/lib/ai/context/dependencies/config_files/php_composer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..29714da17ab625651557561f07dda660754de2e8 --- /dev/null +++ b/ee/spec/lib/ai/context/dependencies/config_files/php_composer_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::Context::Dependencies::ConfigFiles::PhpComposer, feature_category: :code_suggestions do + it 'returns the expected language value' do + expect(described_class.lang).to eq('php') + end + + it_behaves_like 'parsing a valid dependency config file' do + let(:config_file_content) do + <<~JSON + { + "name": "composer/composer", + "type": "library", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "keywords": [ + "package", + "dependency", + "autoload" + ], + "require": { + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + } + } + JSON + end + + let(:expected_formatted_lib_names) do + ['ext-pcre (*)', 'php (^7.2 || ^8.0)'] + end + end + + context 'when the content contains dev dependencies' do + it_behaves_like 'parsing a valid dependency config file' do + let(:config_file_content) do + <<~JSON + { + "name": "composer/composer", + "type": "library", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "keywords": [ + "package", + "dependency", + "autoload" + ], + "require": { + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "psr/log": "^1.0 || ^2.0 || ^3.0" + } + } + JSON + end + + let(:expected_formatted_lib_names) do + ['ext-pcre (*)', 'php (^7.2 || ^8.0)', 'phpstan/phpstan (^1.10)', 'psr/log (^1.0 || ^2.0 || ^3.0)'] + end + end + end + + context 'when config file content is an array' do + it_behaves_like 'parsing an invalid dependency config file' do + let(:invalid_config_file_content) { '[]' } + let(:expected_parsing_error_message) { 'encountered invalid node' } + end + end + + it_behaves_like 'parsing an invalid dependency config file' do + let(:expected_parsing_error_message) { 'content is not valid JSON' } + end + + describe '.matches?' do + using RSpec::Parameterized::TableSyntax + + where(:path, :matches) do + 'composer.json' | true + 'dir/composer.json' | true + 'dir/subdir/composer.json' | true + 'dir/composer.js' | false + 'Composer.json' | false + 'composer_json' | false + 'composer.lock' | false + end + + with_them do + it 'matches the file name glob pattern at various directory levels' do + expect(described_class.matches?(path)).to eq(matches) + end + end + end +end