diff --git a/doc/user/project/repository/code_suggestions/repository_xray.md b/doc/user/project/repository/code_suggestions/repository_xray.md index e593fe12e15ce8a170f11d8e2eb9f690a0d0f90b..5d5ce745cd286286177869a0ffe0098bf58c35b2 100644 --- a/doc/user/project/repository/code_suggestions/repository_xray.md +++ b/doc/user/project/repository/code_suggestions/repository_xray.md @@ -46,6 +46,7 @@ The Repository X-Ray searches a maximum of two directory levels from the reposit | Go | Go Modules | `go.mod` | 17.4 or later | | Java | Gradle | `build.gradle` | 17.4 or later | | Java | Maven | `pom.xml` | 17.4 or later | +| JavaScript | NPM | `package-lock.json` | 17.5 or later | | Kotlin | Gradle | `build.gradle.kts` | 17.5 or later | | PHP | Composer | `composer.lock`, `composer.json` | 17.5 or later | | Python | Conda | `environment.yml` | 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 5b1b8e73561efd2e1a717db019dc986bb07cc72a..5cb1324c8abf10ac772913a01eb32b22bdf65087 100644 --- a/ee/lib/ai/context/dependencies/config_files/constants.rb +++ b/ee/lib/ai/context/dependencies/config_files/constants.rb @@ -26,6 +26,7 @@ module Constants ConfigFiles::GoModules, ConfigFiles::JavaGradle, ConfigFiles::JavaMaven, + ConfigFiles::JavascriptNpmLock, ConfigFiles::KotlinGradle, ConfigFiles::PhpComposerLock, ConfigFiles::PhpComposer, diff --git a/ee/lib/ai/context/dependencies/config_files/javascript_npm_lock.rb b/ee/lib/ai/context/dependencies/config_files/javascript_npm_lock.rb new file mode 100644 index 0000000000000000000000000000000000000000..091e246ebbedb5d2d17cf8cbc0f2f9881a7a404d --- /dev/null +++ b/ee/lib/ai/context/dependencies/config_files/javascript_npm_lock.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Ai + module Context + module Dependencies + module ConfigFiles + class JavascriptNpmLock < Base + NAME_PREFIX = 'node_modules/' + PUBLIC_REGISTRY_PREFIX = 'https://registry.npmjs.org/' + + def self.file_name_glob + 'package-lock.json' + end + + def self.lang_name + 'JavaScript' + end + + private + + ### The first package is always an empty string representing the project itself + ### Example format: + # + # "packages": { + # "": { + # "name": "countly-server", + # "version": "24.5.0" + # }, + # "api/utils/countly-root": { + # "version": "0.1.0" + # }, + # "node_modules/@babel/core/node_modules/convert-source-map": { + # "version": "2.0.0", + # "resolved": "https://registry.npmjs.org/...", + # "integrity": "sha512-...", + # "dev": true, + # "license": "MIT" + # } + # } + # + def extract_libs + parsed = ::Gitlab::Json.parse(content) + + dig_in(parsed, 'packages').try(:filter_map) do |name, dep| + next if name.empty? + next unless dig_in(dep, 'resolved')&.start_with?(PUBLIC_REGISTRY_PREFIX) + + Lib.new(name: name.delete_prefix(NAME_PREFIX), version: dig_in(dep, 'version')) + end + 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/javascript_npm_lock_spec.rb b/ee/spec/lib/ai/context/dependencies/config_files/javascript_npm_lock_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bcc763339a679095b331e074090798713a687c57 --- /dev/null +++ b/ee/spec/lib/ai/context/dependencies/config_files/javascript_npm_lock_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::Context::Dependencies::ConfigFiles::JavascriptNpmLock, feature_category: :code_suggestions do + it 'returns the expected language value' do + expect(described_class.lang).to eq('javascript') + end + + it_behaves_like 'parsing a valid dependency config file' do + let(:config_file_content) do + <<~JSON + { + "name": "countly-server", + "version": "24.5.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "countly-server", + "version": "24.5.0" + }, + "api/utils/countly-root": { + "version": "0.1.0" + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/...", + "integrity": "sha512-...", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/test-package": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/...", + "integrity": "sha512-...", + "dev": true, + "license": "MIT" + } + } + } + JSON + end + + let(:expected_formatted_lib_names) do + ['@babel/core/node_modules/convert-source-map (2.0.0)', '@babel/test-package (1.2.3)'] + 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 + 'package-lock.json' | true + 'dir/package-lock.json' | true + 'dir/subdir/package-lock.json' | true + 'dir/package.json' | false + 'Package-lock.json' | false + 'package_lock.json' | 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