From d6f7b0d5d2261bc36d380442f10a58c3a3275192 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Vilain Date: Thu, 18 Apr 2013 10:49:23 +0200 Subject: [PATCH] SONAR-3893 Fixed excessive collection iteration when decorating souce --- .../core/source/DecorationDataHolder.java | 48 ++- .../sonar/core/source/HtmlTextDecorator.java | 24 +- .../core/source/DecorationDataHolderTest.java | 36 ++- .../main/webapp/WEB-INF/config/database.yml | 8 +- .../main/webapp/WEB-INF/config/environment.rb | 275 +++++++++--------- 5 files changed, 216 insertions(+), 175 deletions(-) diff --git a/sonar-core/src/main/java/org/sonar/core/source/DecorationDataHolder.java b/sonar-core/src/main/java/org/sonar/core/source/DecorationDataHolder.java index d151cbf266a..41e70dd6bb2 100644 --- a/sonar-core/src/main/java/org/sonar/core/source/DecorationDataHolder.java +++ b/sonar-core/src/main/java/org/sonar/core/source/DecorationDataHolder.java @@ -21,24 +21,22 @@ package org.sonar.core.source; import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; -import java.util.Arrays; -import java.util.List; +import java.util.*; public class DecorationDataHolder { private static final String ENTITY_SEPARATOR = ";"; private static final String FIELD_SEPARATOR = ","; private static final String SYMBOL_PREFIX = "symbol-"; + private static final String HIGHLIGHTABLE = "highlightable"; - private final Multimap lowerBoundsDefinitions; - private final List upperBoundsDefinitions; + private LinkedList tagEntriesStack; + private LinkedList closingTagsStack; public DecorationDataHolder() { - lowerBoundsDefinitions = TreeMultimap.create(); - upperBoundsDefinitions = Lists.newArrayList(); + tagEntriesStack = Lists.newLinkedList(); + closingTagsStack = Lists.newLinkedList(); } public void loadSymbolReferences(String symbolsReferences) { @@ -57,25 +55,43 @@ public class DecorationDataHolder { String[] rules = syntaxHighlightingRules.split(ENTITY_SEPARATOR); for (int i = 0; i < rules.length; i++) { String[] ruleFields = rules[i].split(FIELD_SEPARATOR); - lowerBoundsDefinitions.put(Integer.parseInt(ruleFields[0]), ruleFields[2]); - upperBoundsDefinitions.add(Integer.parseInt(ruleFields[1])); + insertAndPreserveOrder(new TagEntry(Integer.parseInt(ruleFields[0]), ruleFields[2]), tagEntriesStack); + insertAndPreserveOrder(Integer.parseInt(ruleFields[1]), closingTagsStack); } } - public Multimap getLowerBoundsDefinitions() { - return lowerBoundsDefinitions; + public Deque getTagEntriesStack() { + return tagEntriesStack; } - public List getUpperBoundsDefinitions() { - return upperBoundsDefinitions; + public Deque getClosingTagsStack() { + return closingTagsStack; } private void loadSymbolOccurrences(int declarationStartOffset, int symbolLength, String[] symbolOccurrences) { for (int i = 0; i < symbolOccurrences.length; i++) { int occurrenceStartOffset = Integer.parseInt(symbolOccurrences[i]); int occurrenceEndOffset = occurrenceStartOffset + symbolLength; - lowerBoundsDefinitions.put(occurrenceStartOffset, SYMBOL_PREFIX + declarationStartOffset + " highlightable"); - upperBoundsDefinitions.add(occurrenceEndOffset); + insertAndPreserveOrder(new TagEntry(occurrenceStartOffset, SYMBOL_PREFIX + declarationStartOffset + " " + HIGHLIGHTABLE), tagEntriesStack); + insertAndPreserveOrder(occurrenceEndOffset, closingTagsStack); } } + + private void insertAndPreserveOrder(TagEntry newEntry, LinkedList orderedEntries) { + int insertionIndex = 0; + Iterator entriesIterator = orderedEntries.iterator(); + while(entriesIterator.hasNext() && entriesIterator.next().getStartOffset() <= newEntry.getStartOffset()) { + insertionIndex++; + } + orderedEntries.add(insertionIndex, newEntry); + } + + private void insertAndPreserveOrder(int newOffset, LinkedList orderedOffsets) { + int insertionIndex = 0; + Iterator entriesIterator = orderedOffsets.iterator(); + while(entriesIterator.hasNext() && entriesIterator.next() <= newOffset) { + insertionIndex++; + } + orderedOffsets.add(insertionIndex, newOffset); + } } diff --git a/sonar-core/src/main/java/org/sonar/core/source/HtmlTextDecorator.java b/sonar-core/src/main/java/org/sonar/core/source/HtmlTextDecorator.java index af6539bc991..2dffdb69373 100644 --- a/sonar-core/src/main/java/org/sonar/core/source/HtmlTextDecorator.java +++ b/sonar-core/src/main/java/org/sonar/core/source/HtmlTextDecorator.java @@ -28,7 +28,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.util.Collection; -import java.util.Collections; import java.util.List; import static org.sonar.core.source.CharactersReader.END_OF_STREAM; @@ -47,7 +46,7 @@ public class HtmlTextDecorator { public static final String ENCODED_HTML_CLOSING = ">"; public static final String ENCODED_AMPERSAND = "&"; - public List decorateTextWithHtml(String text, DecorationDataHolder context) { + public List decorateTextWithHtml(String text, DecorationDataHolder decorationDataHolder) { StringBuilder currentHtmlLine = new StringBuilder(); List decoratedHtmlLines = Lists.newArrayList(); @@ -69,14 +68,14 @@ public class HtmlTextDecorator { } } - int numberOfTagsToClose = getNumberOfTagsToClose(charsReader.getCurrentIndex(), context); + int numberOfTagsToClose = getNumberOfTagsToClose(charsReader.getCurrentIndex(), decorationDataHolder); closeCompletedTags(charsReader, numberOfTagsToClose, currentHtmlLine); if (shouldClosePendingTags(charsReader)) { closeCurrentSyntaxTags(charsReader, currentHtmlLine); } - Collection tagsToOpen = getTagsToOpen(charsReader.getCurrentIndex(), context); + Collection tagsToOpen = getTagsToOpen(charsReader.getCurrentIndex(), decorationDataHolder); openNewTags(charsReader, tagsToOpen, currentHtmlLine); if (shouldAppendCharToHtmlOutput(charsReader)) { @@ -119,12 +118,21 @@ public class HtmlTextDecorator { return charsReader.getCurrentValue() != CR_END_OF_LINE && charsReader.getCurrentValue() != LF_END_OF_LINE; } - private int getNumberOfTagsToClose(int currentIndex, DecorationDataHolder context) { - return Collections.frequency(context.getUpperBoundsDefinitions(), currentIndex); + private int getNumberOfTagsToClose(int currentIndex, DecorationDataHolder dataHolder) { + int numberOfTagsToClose = 0; + while(!dataHolder.getClosingTagsStack().isEmpty() && dataHolder.getClosingTagsStack().peek() == currentIndex) { + dataHolder.getClosingTagsStack().pop(); + numberOfTagsToClose++; + } + return numberOfTagsToClose; } - private Collection getTagsToOpen(int currentIndex, DecorationDataHolder context) { - return context.getLowerBoundsDefinitions().get(currentIndex); + private Collection getTagsToOpen(int currentIndex, DecorationDataHolder dataHolder) { + Collection tagsToOpen = Lists.newArrayList(); + while(!dataHolder.getTagEntriesStack().isEmpty() && dataHolder.getTagEntriesStack().peek().getStartOffset() == currentIndex) { + tagsToOpen.add(dataHolder.getTagEntriesStack().pop().getCssClass()); + } + return tagsToOpen; } private boolean shouldClosePendingTags(CharactersReader charactersReader) { diff --git a/sonar-core/src/test/java/org/sonar/core/source/DecorationDataHolderTest.java b/sonar-core/src/test/java/org/sonar/core/source/DecorationDataHolderTest.java index db5cfb9e499..7e72f703064 100644 --- a/sonar-core/src/test/java/org/sonar/core/source/DecorationDataHolderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/source/DecorationDataHolderTest.java @@ -20,11 +20,10 @@ package org.sonar.core.source; -import com.google.common.collect.Multimap; import org.junit.Before; import org.junit.Test; -import java.util.List; +import java.util.Deque; import static org.fest.assertions.Assertions.assertThat; @@ -45,23 +44,32 @@ public class DecorationDataHolderTest { @Test public void should_extract_lower_bounds_from_serialized_rules() throws Exception { - Multimap lowerBoundsDefinitions = decorationDataHolder.getLowerBoundsDefinitions(); + Deque tagEntries = decorationDataHolder.getTagEntriesStack(); - assertThat(lowerBoundsDefinitions.containsEntry(0, "k")); - assertThat(lowerBoundsDefinitions.containsEntry(0, "cppd")); - assertThat(lowerBoundsDefinitions.containsEntry(54, "a")); - assertThat(lowerBoundsDefinitions.containsEntry(69, "k")); - assertThat(lowerBoundsDefinitions.containsEntry(80, "symbol-80 highlightable")); - assertThat(lowerBoundsDefinitions.containsEntry(90, "symbol-90 highlightable")); - assertThat(lowerBoundsDefinitions.containsEntry(106, "cppd")); - assertThat(lowerBoundsDefinitions.containsEntry(114, "k")); - assertThat(lowerBoundsDefinitions.containsEntry(140, "symbol-140 highlightable")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(0, "k")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(0, "cppd")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(54, "a")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(69, "k")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(80, "symbol-80 highlightable")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(90, "symbol-80 highlightable")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(106, "cppd")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(114, "k")); + assertThat(tagEntries.pop()).isEqualTo(new TagEntry(140, "symbol-80 highlightable")); } @Test public void should_extract_upper_bounds_from_serialized_rules() throws Exception { - List upperBoundsDefinition = decorationDataHolder.getUpperBoundsDefinitions(); - assertThat(upperBoundsDefinition).containsExactly(8, 52, 67, 75, 130, 130, 85, 95, 145); + Deque upperBoundsDefinition = decorationDataHolder.getClosingTagsStack(); + + assertThat(upperBoundsDefinition.pop()).isEqualTo(8); + assertThat(upperBoundsDefinition.pop()).isEqualTo(52); + assertThat(upperBoundsDefinition.pop()).isEqualTo(67); + assertThat(upperBoundsDefinition.pop()).isEqualTo(75); + assertThat(upperBoundsDefinition.pop()).isEqualTo(85); + assertThat(upperBoundsDefinition.pop()).isEqualTo(95); + assertThat(upperBoundsDefinition.pop()).isEqualTo(130); + assertThat(upperBoundsDefinition.pop()).isEqualTo(130); + assertThat(upperBoundsDefinition.pop()).isEqualTo(145); } } diff --git a/sonar-server/src/main/webapp/WEB-INF/config/database.yml b/sonar-server/src/main/webapp/WEB-INF/config/database.yml index 5fdee0d5d9e..46c51b5a9dc 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/database.yml +++ b/sonar-server/src/main/webapp/WEB-INF/config/database.yml @@ -13,4 +13,10 @@ development: <<: *base production: - <<: *base \ No newline at end of file + <<: *base + +test: + adapter: sqlite3 + database: db/test.sqlite3 + pool: 5 + timeout: 5000 \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb index 7e146218751..d895ddf88ff 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb @@ -1,160 +1,163 @@ RAILS_GEM_VERSION = '2.3.15' -# Avoid conflict with local ruby installations -# See http://jira.codehaus.org/browse/SONAR-3579 -ENV['GEM_HOME'] = $servlet_context.getRealPath('/WEB-INF/gems') - # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') -require 'color' - -# Disable all the warnings : -# Gem::SourceIndex#initialize called from /.../war/sonar-server/WEB-INF/gems/gems/rails-2.3.15/lib/rails/vendor_gem_source_index.rb:100. -# The other solutions are to upgrade to rails 3 or to use gembundler.com -require 'rubygems' -Gem::Deprecate.skip = (RAILS_ENV == 'production') - -# -# Limitation of Rails 2.3 and Rails Engines (plugins) when threadsafe! is enabled in production mode -# See http://groups.google.com/group/rubyonrails-core/browse_thread/thread/9067bce01444fb24?pli=1 -# -class EagerPluginLoader < Rails::Plugin::Loader - def add_plugin_load_paths - super - plugins.each do |plugin| - if configuration.cache_classes - configuration.eager_load_paths += plugin.load_paths + +unless ENV["RAILS_ENV"] = "test" + + # Avoid conflict with local ruby installations + # See http://jira.codehaus.org/browse/SONAR-3579 + ENV['GEM_HOME'] = $servlet_context.getRealPath('/WEB-INF/gems') + + require 'color' + + # Disable all the warnings : + # Gem::SourceIndex#initialize called from /.../war/sonar-server/WEB-INF/gems/gems/rails-2.3.15/lib/rails/vendor_gem_source_index.rb:100. + # The other solutions are to upgrade to rails 3 or to use gembundler.com + require 'rubygems' + Gem::Deprecate.skip = (RAILS_ENV == 'production') + + # + # Limitation of Rails 2.3 and Rails Engines (plugins) when threadsafe! is enabled in production mode + # See http://groups.google.com/group/rubyonrails-core/browse_thread/thread/9067bce01444fb24?pli=1 + # + class EagerPluginLoader < Rails::Plugin::Loader + def add_plugin_load_paths + super + plugins.each do |plugin| + if configuration.cache_classes + configuration.eager_load_paths += plugin.load_paths + end end end end -end -Rails::Initializer.run do |config| - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - # See Rails::Configuration for more options. - - # Skip frameworks you're not going to use. To use Rails without a database - # you must remove the Active Record framework. - config.frameworks -= [ :action_mailer ] - - # This property can't be set in config/environments because of execution order - # See http://strd6.com/2009/04/cant-dup-nilclass-maybe-try-unloadable/ - config.reload_plugins=(RAILS_ENV == 'development') - - config.plugin_loader = EagerPluginLoader - - # Load the applications that are packaged with sonar plugins. - # The development mode (real-time edition of ruby code) can be enabled on an app by replacing the - # following line by : - # config.plugin_paths << '/absolute/path/to/myproject/src/main/resources/org/sonar/ror' - config.plugin_paths << "#{Java::OrgSonarServerUi::JRubyFacade.getInstance().getServerHome()}/temp/ror" - - # Force all environments to use the same logger level - # (by default production uses :info, the others :debug) - # config.log_level = :debug - - # Make Time.zone default to the specified zone, and make Active Record store time values - # in the database in UTC, and return them converted to the specified local zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time. - # config.time_zone = 'UTC' - - # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths. - # All files from config/locales/*.rb,yml are added automatically. - - # Default locales provided by Ruby on Rails - config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '**', '*.{rb,yml}')] - - # Overridden bundles - config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '*.{rb,yml}')] - - config.i18n.default_locale = :en - - # Provided by JRuby-Rack - config.action_controller.session_store = :java_servlet_store - - # Use SQL instead of Active Record's schema dumper when creating the test database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - - # Activate observers that should always be running - # Please note that observers generated using script/generate observer need to have an _observer suffix - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer -end + Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + # See Rails::Configuration for more options. + # Skip frameworks you're not going to use. To use Rails without a database + # you must remove the Active Record framework. + config.frameworks -= [:action_mailer] + # This property can't be set in config/environments because of execution order + # See http://strd6.com/2009/04/cant-dup-nilclass-maybe-try-unloadable/ + config.reload_plugins=(RAILS_ENV == 'development') -class ActiveRecord::Migration - def self.dialect - ActiveRecord::Base.configurations[ ENV['RAILS_ENV'] ]['dialect'] - end + config.plugin_loader = EagerPluginLoader + + # Load the applications that are packaged with sonar plugins. + # The development mode (real-time edition of ruby code) can be enabled on an app by replacing the + # following line by : + # config.plugin_paths << '/absolute/path/to/myproject/src/main/resources/org/sonar/ror' + config.plugin_paths << "#{Java::OrgSonarServerUi::JRubyFacade.getInstance().getServerHome()}/temp/ror" + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Make Time.zone default to the specified zone, and make Active Record store time values + # in the database in UTC, and return them converted to the specified local zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time. + # config.time_zone = 'UTC' + + # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths. + # All files from config/locales/*.rb,yml are added automatically. + + # Default locales provided by Ruby on Rails + config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '**', '*.{rb,yml}')] + + # Overridden bundles + config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '*.{rb,yml}')] + + config.i18n.default_locale = :en + + # Provided by JRuby-Rack + config.action_controller.session_store = :java_servlet_store + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql - def self.add_index(table_name, column_name, options = {}) - # ActiveRecord can generate index names longer than 30 characters, but that's - # not supported by Oracle, the "Enterprise" database. - # For this reason we force to set name of indexes. - raise ArgumentError, 'Missing index name' unless options[:name] - super(table_name, column_name, options) + # Activate observers that should always be running + # Please note that observers generated using script/generate observer need to have an _observer suffix + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer end - def self.alter_to_big_primary_key(tablename) - case dialect() - when "postgre" - execute "ALTER TABLE #{tablename} ALTER COLUMN id TYPE bigint" - when "mysql" - execute "ALTER TABLE #{tablename} CHANGE id id BIGINT AUTO_INCREMENT" - when "h2" - # not needed? - when "oracle" - # do nothing, oracle integer are big enough - when "sqlserver" - constraint=select_one "select name from sysobjects where parent_obj = (select id from sysobjects where name = '#{tablename}')" - execute "ALTER TABLE #{tablename} DROP CONSTRAINT #{constraint["name"]}" - execute "ALTER TABLE #{tablename} ALTER COLUMN id bigint" - execute "ALTER TABLE #{tablename} ADD PRIMARY KEY(id)" + class ActiveRecord::Migration + def self.dialect + ActiveRecord::Base.configurations[ENV['RAILS_ENV']]['dialect'] + end + + def self.add_index(table_name, column_name, options = {}) + # ActiveRecord can generate index names longer than 30 characters, but that's + # not supported by Oracle, the "Enterprise" database. + # For this reason we force to set name of indexes. + raise ArgumentError, 'Missing index name' unless options[:name] + super(table_name, column_name, options) end - end - def self.alter_to_big_integer(tablename, columnname, indexname=nil) - case dialect() - when "sqlserver" - execute "DROP INDEX #{indexname} on #{tablename}" if indexname - change_column(tablename, columnname, :big_integer, :null => true) - execute "CREATE INDEX #{indexname} on #{tablename}(#{columnname})" if indexname - else - change_column(tablename, columnname, :big_integer, :null => true) - end + def self.alter_to_big_primary_key(tablename) + case dialect() + when "postgre" + execute "ALTER TABLE #{tablename} ALTER COLUMN id TYPE bigint" + when "mysql" + execute "ALTER TABLE #{tablename} CHANGE id id BIGINT AUTO_INCREMENT" + when "h2" + # not needed? + when "oracle" + # do nothing, oracle integer are big enough + when "sqlserver" + constraint=select_one "select name from sysobjects where parent_obj = (select id from sysobjects where name = '#{tablename}')" + execute "ALTER TABLE #{tablename} DROP CONSTRAINT #{constraint["name"]}" + execute "ALTER TABLE #{tablename} ALTER COLUMN id bigint" + execute "ALTER TABLE #{tablename} ADD PRIMARY KEY(id)" + end + end + + def self.alter_to_big_integer(tablename, columnname, indexname=nil) + case dialect() + when "sqlserver" + execute "DROP INDEX #{indexname} on #{tablename}" if indexname + change_column(tablename, columnname, :big_integer, :null => true) + execute "CREATE INDEX #{indexname} on #{tablename}(#{columnname})" if indexname + else + change_column(tablename, columnname, :big_integer, :null => true) + end + end end -end -# patch for SONAR-1182. GWT does not support ISO8601 dates that end with 'Z' -# http://google-web-toolkit.googlecode.com/svn/javadoc/1.6/com/google/gwt/i18n/client/DateTimeFormat.html -module ActiveSupport - class TimeWithZone - def xmlschema - # initial code: "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}" - "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, nil)}" + # patch for SONAR-1182. GWT does not support ISO8601 dates that end with 'Z' + # http://google-web-toolkit.googlecode.com/svn/javadoc/1.6/com/google/gwt/i18n/client/DateTimeFormat.html + module ActiveSupport + class TimeWithZone + def xmlschema + # initial code: "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}" + "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, nil)}" + end end end -end -# -# other patches : -# - activerecord : fix Oracle bug when more than 1000 elements in IN clause. See lib/active_record/association_preload.rb -# See https://github.com/rails/rails/issues/585 -# - actionview NumberHelper, patch for number_with_precision() + # + # other patches : + # - activerecord : fix Oracle bug when more than 1000 elements in IN clause. See lib/active_record/association_preload.rb + # See https://github.com/rails/rails/issues/585 + # - actionview NumberHelper, patch for number_with_precision() + + require File.dirname(__FILE__) + '/../lib/sonar_webservice_plugins.rb' + require File.dirname(__FILE__) + '/../lib/database_version.rb' + DatabaseVersion.automatic_setup -require File.dirname(__FILE__) + '/../lib/sonar_webservice_plugins.rb' -require File.dirname(__FILE__) + '/../lib/database_version.rb' -DatabaseVersion.automatic_setup + # + # + # IMPORTANT NOTE + # Some changes have been done in activerecord-jdbc-adapter. Most of them relate to column types. + # All these changes are prefixed by the comment #sonar + # + # -# -# -# IMPORTANT NOTE -# Some changes have been done in activerecord-jdbc-adapter. Most of them relate to column types. -# All these changes are prefixed by the comment #sonar -# -# \ No newline at end of file +end -- 2.39.5