From 3ab3b4231c1e60a865e2562e17461788cb9fce18 Mon Sep 17 00:00:00 2001 From: Andrey Luiz Date: Mon, 13 Nov 2023 10:34:28 +0100 Subject: [PATCH] SONAR-20607 Add option to optimize quality gates CaYC compliant without 0 issues conditions (#9929) --- .../js/api/mocks/QualityGatesServiceMock.ts | 18 ++ .../components/CaycCompliantBanner.tsx | 62 +++++++ .../components/CaycConditionsListItem.tsx | 32 ---- .../components/CaycFixOptimizeBanner.tsx | 71 ++++++++ .../ConditionReviewAndUpdateModal.tsx | 19 +- .../components/ConditionValueDescription.tsx | 50 +---- .../quality-gates/components/Conditions.tsx | 85 +++------ .../components/__tests__/QualityGate-it.tsx | 69 +++++++ .../src/main/js/apps/quality-gates/utils.ts | 172 ++++++++++++------ .../resources/org/sonar/l10n/core.properties | 6 + 10 files changed, 385 insertions(+), 199 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts index 6aedf5dcd25..5b4ce44f7e2 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -125,6 +125,13 @@ export class QualityGatesServiceMock { mockQualityGate({ name: 'SonarSource way - CFamily', conditions: [ + { + id: 'AXJMbIUHPAOIsUIE3eOi', + metric: 'new_security_hotspots_reviewed', + op: 'LT', + error: '85', + isCaycCondition: true, + }, { id: 'AXJMbIUHPAOIsUIE3eOu', metric: 'new_coverage', @@ -187,6 +194,17 @@ export class QualityGatesServiceMock { isBuiltIn: false, caycStatus: CaycStatus.NonCompliant, }), + mockQualityGate({ + name: 'Non Cayc Compliant QG', + conditions: [ + { id: 'AXJMbIUHPAOIsUIE3eNs', metric: 'new_security_rating', op: 'GT', error: '1' }, + { id: 'AXJMbIUHPAOIsUIE3eOD', metric: 'new_reliability_rating', op: 'GT', error: '1' }, + { id: 'AXJMbIUHPAOIsUIE3eOF', metric: 'new_coverage', op: 'LT', error: '80' }, + ], + isDefault: false, + isBuiltIn: false, + caycStatus: CaycStatus.Compliant, + }), mockQualityGate({ name: 'Over Compliant CAYC QG', conditions: [ diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx new file mode 100644 index 00000000000..d9b9d6d9cea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { + CardWithPrimaryBackground, + CheckIcon, + LightLabel, + SubHeadingHighlight, +} from 'design-system'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DocumentationLink from '../../../components/common/DocumentationLink'; +import { translate } from '../../../helpers/l10n'; +import { OPTIMIZED_CAYC_CONDITIONS } from '../utils'; + +export default function CaycCompliantBanner() { + return ( + + + {translate('quality_gates.cayc.banner.title')} + + +
+ + {translate('quality_gates.cayc')} + + ), + }} + /> +
+
{translate('quality_gates.cayc.banner.description2')}
+ +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx deleted file mode 100644 index 0b61159c9e0..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import { CheckIcon, LightLabel } from 'design-system'; -import * as React from 'react'; -import { translate } from '../../../helpers/l10n'; - -export default function CaycConditionsListItem({ metricKey }: Readonly<{ metricKey: string }>) { - return ( -
  • - - {translate(`metric.${metricKey}.description.positive`)} -
  • - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx new file mode 100644 index 00000000000..b388cc2bbea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ButtonPrimary, CardWithPrimaryBackground, SubHeadingHighlight } from 'design-system'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DocumentationLink from '../../../components/common/DocumentationLink'; +import ModalButton from '../../../components/controls/ModalButton'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + renderCaycModal: ({ onClose }: { onClose: () => void }) => React.ReactNode; + isOptimizing?: boolean; +} + +export default function CaycNonCompliantBanner({ renderCaycModal, isOptimizing }: Readonly) { + return ( + + + {translate( + isOptimizing + ? 'quality_gates.cayc_optimize.banner.title' + : 'quality_gates.cayc_missing.banner.title', + )} + +
    + + {translate('quality_gates.cayc')} + + ), + }} + /> +
    + + {({ onClick }) => ( + + {translate( + isOptimizing + ? 'quality_gates.cayc_condition.review_optimize' + : 'quality_gates.cayc_condition.review_update', + )} + + )} + +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx index 384f664ca90..63b9d78ca14 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx @@ -40,6 +40,7 @@ interface Props { onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void; lockEditing: () => void; qualityGate: QualityGate; + isOptimizing?: boolean; } export default function CaycReviewUpdateConditionsModal(props: Readonly) { @@ -51,6 +52,7 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly) onAddCondition, lockEditing, onClose, + isOptimizing, } = props; const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); @@ -108,8 +110,11 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly)
    @@ -164,7 +169,9 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly) ) type="submit" onClick={updateCaycQualityGate} > - {translate('quality_gates.cayc.review_update_modal.confirm_text')} + {translate( + isOptimizing + ? 'quality_gates.cayc.review_optimize_modal.confirm_text' + : 'quality_gates.cayc.review_update_modal.confirm_text', + )} } secondaryButtonLabel={translate('close')} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx index 64d7d220d11..61673b91d88 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx @@ -18,17 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; -import { - GRID_INDEX_OFFSET, - PERCENT_MULTIPLIER, - getMaintainabilityGrid, -} from '../../../helpers/ratings'; -import { AppState } from '../../../types/appstate'; -import { MetricKey, MetricType } from '../../../types/metrics'; -import { GlobalSettingKeys } from '../../../types/settings'; +import { MetricKey } from '../../../types/metrics'; import { Condition, Metric } from '../../../types/types'; import { GreenColorText } from './ConditionValue'; @@ -37,50 +29,22 @@ const NO_DESCRIPTION_CONDITION = [ MetricKey.new_security_hotspots_reviewed, MetricKey.new_coverage, MetricKey.new_duplicated_lines_density, + MetricKey.new_reliability_rating, + MetricKey.new_security_rating, + MetricKey.new_maintainability_rating, ]; interface Props { - appState: AppState; condition: Condition; metric: Metric; isToBeModified?: boolean; } -function ConditionValueDescription({ +export default function ConditionValueDescription({ condition, - appState: { settings }, metric, isToBeModified = false, }: Readonly) { - if (condition.metric === MetricKey.new_maintainability_rating) { - const maintainabilityGrid = getMaintainabilityGrid( - settings[GlobalSettingKeys.RatingGrid] ?? '', - ); - const maintainabilityRatingThreshold = - maintainabilityGrid[Math.floor(Number(condition.error)) - GRID_INDEX_OFFSET]; - const ratingLetter = formatMeasure(condition.error, MetricType.Rating); - - return ( - - ( - {condition.error === '1' - ? translateWithParameters( - 'quality_gates.cayc.new_maintainability_rating.A', - formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, MetricType.Percent), - ) - : translateWithParameters( - 'quality_gates.cayc.new_maintainability_rating', - ratingLetter, - formatMeasure( - maintainabilityRatingThreshold * PERCENT_MULTIPLIER, - MetricType.Percent, - ), - )} - ) - - ); - } - return ( {condition.isCaycCondition && @@ -99,5 +63,3 @@ function ConditionValueDescription({ ); } - -export default withAppStateContext(ConditionValueDescription); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index cf714dbaaf5..c3b9ec5c052 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -19,9 +19,7 @@ */ import { - ButtonPrimary, ButtonSecondary, - CardWithPrimaryBackground, FlagMessage, HeadingDark, HelperHintIcon, @@ -29,7 +27,6 @@ import { Link, Note, SubHeading, - SubHeadingHighlight, } from 'design-system'; import { differenceWith, map, uniqBy } from 'lodash'; import * as React from 'react'; @@ -51,10 +48,11 @@ import { Metric, QualityGate, } from '../../../types/types'; -import { CAYC_CONDITIONS, groupAndSortByPriorityConditions } from '../utils'; +import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils'; import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide'; -import CaycConditionsListItem from './CaycConditionsListItem'; +import CaycCompliantBanner from './CaycCompliantBanner'; import CaycConditionsTable from './CaycConditionsTable'; +import CaycFixOptimizeBanner from './CaycFixOptimizeBanner'; import ConditionModal from './ConditionModal'; import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; import ConditionsTable from './ConditionsTable'; @@ -76,7 +74,7 @@ const FORBIDDEN_METRICS: string[] = [ MetricKey.new_security_hotspots, ]; -export function Conditions({ +function Conditions({ qualityGate, metrics, onRemoveCondition, @@ -109,8 +107,6 @@ export function Conditions({ metric: metrics[condition.metric], })); - const getDocUrl = useDocUrl(); - // set edit only when the name is change // i.e when user changes the quality gate React.useEffect(() => { @@ -143,6 +139,11 @@ export function Conditions({ [metrics, qualityGate, onAddCondition], ); + const getDocUrl = useDocUrl(); + const isCompliantCustomQualityGate = + qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn; + const isOptimizing = isCompliantCustomQualityGate && !isQualityGateOptimized(qualityGate); + const renderCaycModal = React.useCallback( ({ onClose }: ModalProps) => { const { conditions = [] } = qualityGate; @@ -160,71 +161,31 @@ export function Conditions({ conditions={conditions} scope="new-cayc" onClose={onClose} + isOptimizing={isOptimizing} /> ); }, - [qualityGate, metrics, updatedConditionId, onAddCondition, onRemoveCondition, onSaveCondition], + [ + qualityGate, + metrics, + updatedConditionId, + onAddCondition, + onRemoveCondition, + onSaveCondition, + isOptimizing, + ], ); return (
    - {qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn && ( - - - {translate('quality_gates.cayc.banner.title')} - - -
    - - {translate('quality_gates.cayc')} - - ), - }} - /> -
    -
    {translate('quality_gates.cayc.banner.description2')}
    -
      - {Object.values(CAYC_CONDITIONS).map((condition) => ( - - ))} -
    -
    + {isCompliantCustomQualityGate && !isOptimizing && } + {isCompliantCustomQualityGate && isOptimizing && canEdit && ( + )} {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( - - - {translate('quality_gates.cayc_missing.banner.title')} - -
    - - {translate('quality_gates.cayc')} - - ), - }} - /> -
    - {canEdit && ( - - {({ onClick }) => ( - - {translate('quality_gates.cayc_condition.review_update')} - - )} - - )} -
    + )}
    diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index ce39c5ec511..f6e563cc422 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -379,6 +379,44 @@ it('should show warning banner when CAYC condition is not properly set and shoul expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument(); }); +it('should show optimize banner when CAYC condition is not properly set and QG is compliant and should be able to update them', async () => { + const user = userEvent.setup(); + qualityGateHandler.setIsAdmin(true); + renderQualityGateApp(); + + const qualityGate = await screen.findByText('Non Cayc Compliant QG'); + + await user.click(qualityGate); + + expect(screen.getByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument(); + expect(screen.getByText('quality_gates.cayc_optimize.banner.description')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }), + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }), + ); + expect( + screen.getByRole('dialog', { + name: 'quality_gates.cayc.review_optimize_modal.header.Non Cayc Compliant QG', + }), + ).toBeInTheDocument(); + expect( + screen.getByText('quality_gates.cayc.review_optimize_modal.description1'), + ).toBeInTheDocument(); + expect( + screen.getByText('quality_gates.cayc.review_update_modal.description2'), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }), + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }), + ); +}); + it('should not warn user when quality gate is not CAYC compliant and user has no permission to edit it', async () => { const user = userEvent.setup(); renderQualityGateApp(); @@ -391,6 +429,20 @@ it('should not warn user when quality gate is not CAYC compliant and user has no expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument(); }); +it('should not show optimize banner when quality gate is compliant but non-CaYC and user has no permission to edit it', async () => { + const user = userEvent.setup(); + renderQualityGateApp(); + + const nonCompliantQualityGate = await screen.findByRole('button', { + name: 'Non Cayc Compliant QG', + }); + + await user.click(nonCompliantQualityGate); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument(); +}); + it('should warn user when quality gate is not CAYC compliant and user has permission to edit it', async () => { const user = userEvent.setup(); qualityGateHandler.setIsAdmin(true); @@ -399,11 +451,28 @@ it('should warn user when quality gate is not CAYC compliant and user has permis const nonCompliantQualityGate = await screen.findByRole('button', { name: /Non Cayc QG/ }); await user.click(nonCompliantQualityGate); + // expect(screen.getByTestId('conditions')).toMatchSnapshot(); expect(await screen.findByText(/quality_gates.cayc_missing.banner.title/)).toBeInTheDocument(); expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); }); +it('should show optimize banner when quality gate is compliant but non-CaYC and user has permission to edit it', async () => { + const user = userEvent.setup(); + qualityGateHandler.setIsAdmin(true); + renderQualityGateApp(); + + const nonCompliantQualityGate = await screen.findByRole('button', { + name: /Non Cayc Compliant QG/, + }); + + await user.click(nonCompliantQualityGate); + // expect(screen.getByTestId('conditions')).toMatchSnapshot(); + + expect(await screen.findByText(/quality_gates.cayc_optimize.banner.title/)).toBeInTheDocument(); + expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); +}); + it('should render CaYC conditions on a separate table if Sonar way', async () => { const user = userEvent.setup(); qualityGateHandler.setIsAdmin(true); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 31ad1ffdae7..3376cbb2267 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -28,23 +28,25 @@ interface GroupedByMetricConditions { caycConditions: Condition[]; } -type CaycMetricKeys = - | MetricKey.new_violations +type CommonCaycMetricKeys = | MetricKey.new_security_hotspots_reviewed | MetricKey.new_coverage | MetricKey.new_duplicated_lines_density; -export const CAYC_CONDITIONS: Record< - CaycMetricKeys, +type OptimizedCaycMetricKeys = MetricKey.new_violations | CommonCaycMetricKeys; + +type UnoptimizedCaycMetricKeys = + | MetricKey.new_reliability_rating + | MetricKey.new_security_rating + | MetricKey.new_maintainability_rating + | CommonCaycMetricKeys; + +type AllCaycMetricKeys = OptimizedCaycMetricKeys | UnoptimizedCaycMetricKeys; + +const COMMON_CONDITIONS: Record< + CommonCaycMetricKeys, Condition & { shouldRenderOperator?: boolean } > = { - [MetricKey.new_violations]: { - id: MetricKey.new_violations, - metric: MetricKey.new_violations, - op: 'GT', - error: '0', - isCaycCondition: true, - }, [MetricKey.new_security_hotspots_reviewed]: { id: MetricKey.new_security_hotspots_reviewed, metric: MetricKey.new_security_hotspots_reviewed, @@ -70,6 +72,56 @@ export const CAYC_CONDITIONS: Record< }, }; +export const OPTIMIZED_CAYC_CONDITIONS: Record< + OptimizedCaycMetricKeys, + Condition & { shouldRenderOperator?: boolean } +> = { + [MetricKey.new_violations]: { + id: MetricKey.new_violations, + metric: MetricKey.new_violations, + op: 'GT', + error: '0', + isCaycCondition: true, + }, + ...COMMON_CONDITIONS, +}; + +const UNOPTIMIZED_CAYC_CONDITIONS: Record< + UnoptimizedCaycMetricKeys, + Condition & { shouldRenderOperator?: boolean } +> = { + [MetricKey.new_reliability_rating]: { + id: MetricKey.new_reliability_rating, + metric: MetricKey.new_reliability_rating, + op: 'GT', + error: '1', + isCaycCondition: true, + }, + [MetricKey.new_security_rating]: { + id: MetricKey.new_security_rating, + metric: MetricKey.new_security_rating, + op: 'GT', + error: '1', + isCaycCondition: true, + }, + [MetricKey.new_maintainability_rating]: { + id: MetricKey.new_maintainability_rating, + metric: MetricKey.new_maintainability_rating, + op: 'GT', + error: '1', + isCaycCondition: true, + }, + ...COMMON_CONDITIONS, +}; + +const ALL_CAYC_CONDITIONS: Record< + AllCaycMetricKeys, + Condition & { shouldRenderOperator?: boolean } +> = { + ...OPTIMIZED_CAYC_CONDITIONS, + ...UNOPTIMIZED_CAYC_CONDITIONS, +}; + const CAYC_CONDITION_ORDER_PRIORITIES: Dict = [ MetricKey.new_violations, MetricKey.new_security_hotspots_reviewed, @@ -79,26 +131,50 @@ const CAYC_CONDITION_ORDER_PRIORITIES: Dict = [ .reverse() .reduce((acc, key, i) => ({ ...acc, [key.toString()]: i + 1 }), {} as Dict); -const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: CaycMetricKeys[] = [ +const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: AllCaycMetricKeys[] = [ MetricKey.new_duplicated_lines_density, MetricKey.new_coverage, ]; -const CAYC_CONDITIONS_WITH_FIXED_VALUE: CaycMetricKeys[] = [ +const CAYC_CONDITIONS_WITH_FIXED_VALUE: AllCaycMetricKeys[] = [ MetricKey.new_security_hotspots_reviewed, MetricKey.new_violations, + MetricKey.new_reliability_rating, + MetricKey.new_security_rating, + MetricKey.new_maintainability_rating, ]; export function isConditionWithFixedValue(condition: Condition) { - return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as CaycMetricKeys); + return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as OptimizedCaycMetricKeys); } export function getCaycConditionMetadata(condition: Condition) { - const foundCondition = CAYC_CONDITIONS[condition.metric as CaycMetricKeys]; + const foundCondition = OPTIMIZED_CAYC_CONDITIONS[condition.metric as OptimizedCaycMetricKeys]; return { shouldRenderOperator: foundCondition?.shouldRenderOperator, }; } +export function isQualityGateOptimized(qualityGate: QualityGate) { + return ( + !qualityGate.isBuiltIn && + qualityGate.caycStatus !== CaycStatus.NonCompliant && + Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => { + const foundCondition = qualityGate.conditions?.find((c) => c.metric === condition.metric); + return ( + foundCondition && + !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition) + ); + }) + ); +} + +function isWeakCondition(key: AllCaycMetricKeys, selectedCondition: Condition) { + return ( + !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && + ALL_CAYC_CONDITIONS[key]?.error !== selectedCondition.error + ); +} + export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { const result: { weakConditions: Condition[]; @@ -107,31 +183,18 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { weakConditions: [], missingConditions: [], }; - Object.keys(CAYC_CONDITIONS).forEach((key: CaycMetricKeys) => { + Object.keys(OPTIMIZED_CAYC_CONDITIONS).forEach((key: OptimizedCaycMetricKeys) => { const selectedCondition = conditions.find((condition) => condition.metric === key); if (!selectedCondition) { - result.missingConditions.push(CAYC_CONDITIONS[key]); - } else if ( - !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && - CAYC_CONDITIONS[key]?.error !== selectedCondition.error - ) { + result.missingConditions.push(OPTIMIZED_CAYC_CONDITIONS[key]); + } else if (isWeakCondition(key, selectedCondition)) { result.weakConditions.push(selectedCondition); } }); return result; } -export function getCaycConditionsWithCorrectValue(conditions: Condition[]) { - return Object.keys(CAYC_CONDITIONS).map((key: CaycMetricKeys) => { - const selectedCondition = conditions.find((condition) => condition.metric === key); - if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && selectedCondition) { - return selectedCondition; - } - return CAYC_CONDITIONS[key]; - }); -} - -export function groupConditionsByMetric( +function groupConditionsByMetric( conditions: Condition[], isBuiltInQG = false, ): GroupedByMetricConditions { @@ -183,11 +246,11 @@ export function groupAndSortByPriorityConditions( } export function getCorrectCaycCondition(condition: Condition) { - const conditionMetric = condition.metric as CaycMetricKeys; + const conditionMetric = condition.metric as OptimizedCaycMetricKeys; if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(conditionMetric)) { return condition; } - return CAYC_CONDITIONS[conditionMetric]; + return OPTIMIZED_CAYC_CONDITIONS[conditionMetric]; } export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate { @@ -225,32 +288,27 @@ export function replaceCondition( return { ...qualityGate, conditions }; } -export function updateCaycCompliantStatus(conditions: Condition[]) { - if (conditions.length < Object.keys(CAYC_CONDITIONS).length) { - return CaycStatus.NonCompliant; - } - - for (const key of Object.keys(CAYC_CONDITIONS)) { - const caycMetric = key as CaycMetricKeys; - const selectedCondition = conditions.find((condition) => condition.metric === key); - if (!selectedCondition) { - return CaycStatus.NonCompliant; - } - - if ( - !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(caycMetric) && - selectedCondition && - selectedCondition.error !== CAYC_CONDITIONS[caycMetric].error - ) { - return CaycStatus.NonCompliant; - } - } +function updateCaycCompliantStatus(conditions: Condition[]) { + const isCompliantOptimized = Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => { + const foundCondition = conditions.find((c) => c.metric === condition.metric); + return ( + foundCondition && + !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition) + ); + }); + const isCompliantUnoptimized = Object.values(UNOPTIMIZED_CAYC_CONDITIONS).every((condition) => { + const foundCondition = conditions.find((c) => c.metric === condition.metric); + return ( + foundCondition && + !isWeakCondition(condition.metric as UnoptimizedCaycMetricKeys, foundCondition) + ); + }); - if (conditions.length > Object.keys(CAYC_CONDITIONS).length) { - return CaycStatus.OverCompliant; + if (isCompliantOptimized || isCompliantUnoptimized) { + return CaycStatus.Compliant; } - return CaycStatus.Compliant; + return CaycStatus.NonCompliant; } export function getPossibleOperators(metric: Metric) { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 49bb2b89cea..11517663e4b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2242,6 +2242,12 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below. quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved +quality_gates.cayc_optimize.banner.title=This quality gate can be further optimized for Clean as You Code +quality_gates.cayc_optimize.banner.description=This quality gate complies with the {cayc_link} methodology, but it can be further optimized to ensure that new code has 0 issues. +quality_gates.cayc_condition.review_optimize=Review and Optimize Quality Gate +quality_gates.cayc.review_optimize_modal.header=Optimize "{0}" for Clean as You Code +quality_gates.cayc.review_optimize_modal.confirm_text=Optimize Quality Gate +quality_gates.cayc.review_optimize_modal.description1=This quality gate will be optimized for {cayc_link}. Please review the changes below. quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready! quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean. quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues -- 2.39.5