Skip to content

Inspección Sonarqube

SepuNelson edited this page Nov 16, 2024 · 2 revisions

Se utilizó Sonarcloud para la inspección del código. Se encontraron varias quality issues de las que destacan 2, "Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed" y "Do not use Array index in keys"


Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed

Esta quality issue identifica un problema de complejidad cognitiva en una función dentro del archivo authContext.tsx. La complejidad cognitiva de esta función es de 19, lo cual supera el límite recomendado de 15. Esto indica que la función tiene demasiadas ramas o condiciones, haciéndola difícil de entender, leer y mantener. La recomendación es refactorizar la función para reducir su complejidad cognitiva al nivel aceptado (15 o menos), mejorando así la mantenibilidad y claridad del código. Esta quality issue posee un severidad Crítica, lo que indica que es altamente peligroso mantenerlo de esta manera.

image

Why (Motivo por el cual Sonarcloud lo ha marcado como un problema)

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code. Code with high cognitive complexity is hard to read, understand, test, and modify.

As a rule of thumb, high cognitive complexity is a sign that the code should be refactored into smaller, easier-to-manage pieces.

Which syntax in code does impact cognitive complexity score? Here are the core concepts:

Cognitive complexity is incremented each time the code breaks the normal linear reading flow. This concerns, for example, loop structures, conditionals, catches, switches, jumps to labels, and conditions mixing multiple operators. Each nesting level increases complexity. During code reading, the deeper you go through nested layers, the harder it becomes to keep the context in mind. Method calls are free A well-picked method name is a summary of multiple lines of code. A reader can first explore a high-level view of what the code is performing then go deeper and deeper by looking at called functions content. Note: This does not apply to recursive calls, those will increment cognitive score. The method of computation is fully detailed in the pdf linked in the resources.

Note that the calculation of cognitive complexity at function level deviates from the documented process. Given the functional nature of JavaScript, nesting functions is a prevalent practice, especially within frameworks like React.js. Consequently, the cognitive complexity of functions remains independent from one another. This means that the complexity of a nesting function does not increase with the complexity of nested functions.

What is the potential impact? Developers spend more time reading and understanding code than writing it. High cognitive complexity slows down changes and increases the cost of maintenance.

Exceptions Cognitive complexity calculations exclude logical expressions using the || and ?? operators when applied within the context of default value code patterns.

function greet(name) {
    name = name || 'Guest';
    console.log('Hello, ' + name + '!');
}

Where (Lugar de identificación del problema)

Problema identificado entre las líneas 82 y 135

setUser({ username: queryResponse.data.username });
  }, [queryResponse.data]);
  const authQuery = useCallback(
    async (path: string, username: string, password: string) => {
Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

      try {
        const response = await axios.post(`${authURL}${path}`, {
          username,
          password,
        });
        
if (response.status >= 300 
|| response.status < 200) {
          
switch (response.status) {
            case 401:
              return 'Unauthorized';
            case 403:
              return 'Forbidden';
            case 404:
              return 'Not Found';
            case 500:
              return 'Internal Server Error';
            default:
              return 'Unknown Error';
          }
        }
        const data: unknown = response.data;
        
if (typeof data !== 'object' 
|| data === null) return 'Invalid response';
        
if (!('token' in data) 
|| !data.token) return 'Invalid token';
        const responseToken = data.token;
        
if (typeof responseToken !== 'string') return 'Invalid token type';
        setToken(responseToken);
        return null;
      } 
catch (error: unknown) {
        
if (error instanceof Error) {
          const message = error.message;
          
if (message.includes('401')) {
            return 'Unauthorized';
          } else 
if (message.includes('403')) {
            return 'Forbidden';
          } else 
if (message.includes('404')) {
            return 'Not Found';
          }
          return message;
        } else 
if (typeof error === 'string') {
          return error;
        } 
else {
          return 'Unknown error';
        }
      }
    },
    [setToken, authURL],

Do not use Array index in keys

Esta quality issue identifica un problema en un componente JSX dentro del archivo PanelGroup.tsx, específicamente en la línea 228. Se está utilizando el índice de un arreglo como clave (key) en una lista, lo cual es una mala práctica. Esto puede generar problemas de renderización y comportamiento inesperado en los componentes si el arreglo se modifica o se reordena. La recomendación es reemplazar el índice por un identificador único que sea inherente a los datos. La solución sugerida es asignar a la propiedad key un valor único y estático que represente de manera consistente a cada elemento. Esta quality issue posee una severidad Mayor, lo que indica que, aunque no es crítica, es importante corregirla para evitar problemas en el futuro.

image

Why (Motivo por el cual Sonarcloud lo ha marcado como un problema)

To optimize the rendering of React list components, a unique identifier (UID) is required for each list item. This UID lets React identify the item throughout its lifetime. Avoid array indexes since the order of the items may change, which will cause keys to not match up between renders, recreating the DOM. It can negatively impact performance and may cause issues with the component state.

function Blog(props) {
  return (
    <ul>
      {props.posts.map((post, index) =>
        <li key={index}> <!-- Noncompliant: When 'posts' are reordered, React will need to recreate the list DOM -->
          {post.title}
        </li>
      )}
    </ul>
  );
}

To fix it, use a string or a number that uniquely identifies the list item. The key must be unique among its siblings, not globally.

If the data comes from a database, database IDs are already unique and are the best option. Otherwise, use a counter or a UUID generator.

function Blog(props) {
  return (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
}

Where (Lugar de identificación del problema)

Problema identificado en el archivo PanelGroup.tsx

/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import Select from 'react-select';
import cornerstone from 'cornerstone-core';
import dicomParser from 'dicom-parser';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import DropInput from '../components/DropInput';
import Panel from './Panel';
import axios from 'axios';
interface PanelGroupProps {
  columns: number;
  rows: number;
}
const PanelGroup: React.FC<PanelGroupProps> = ({ columns, rows }) => {
  useEffect(() => {
    cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
    cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
  }, []);
  const [selectedImageSets, setSelectedImageSets] = React.useState<number[]>(Array(columns * rows).fill(null));
  const [imgSets, setImgSets] = React.useState<string[][]>([]);
  const [imgs, setImgs] = React.useState<
    Record<number, { imageId: string; instanceNumber: number; patientName: string; seriesDescription: string; modality: string; studyDate: string }[]>
  >({});
  const [selectedPatient, setSelectedPatient] = useState<string | null>(null);
  const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
  const [selectedModality, setSelectedModality] = useState<string | null>(null);
  const [selectedStudyDate, setSelectedStudyDate] = useState<string | null>(null);
  const filterOptions = useMemo(() => {
    const uniqueOptions = {
      patients: new Set<string>(),
      series: new Set<string>(),
      modalities: new Set<string>(),
      dates: new Set<string>()
    };
    Object.values(imgs).forEach(imageSet => {
      imageSet.forEach(img => {
        uniqueOptions.patients.add(img.patientName);
        uniqueOptions.series.add(img.seriesDescription);
        uniqueOptions.modalities.add(img.modality);
        uniqueOptions.dates.add(img.studyDate);
      });
    });
    return {
      patients: Array.from(uniqueOptions.patients).map(value => ({ value, label: value })),
      series: Array.from(uniqueOptions.series).map(value => ({ value, label: value })),
      modalities: Array.from(uniqueOptions.modalities).map(value => ({ value, label: value })),
      dates: Array.from(uniqueOptions.dates).map(value => ({ value, label: value }))
    };
  }, [imgs]);
  const getFilteredOptions = useCallback(() => {
    return Object.keys(imgSets)
      .filter((key) => imgs[parseInt(key, 10)]?.length > 0)
      .filter((key) => {
        const imageSet = imgs[parseInt(key, 10)];
        return imageSet.some(img => 
          (!selectedPatient || img.patientName === selectedPatient) &&
          (!selectedSeries || img.seriesDescription === selectedSeries) &&
          (!selectedModality || img.modality === selectedModality) &&
          (!selectedStudyDate || img.studyDate === selectedStudyDate)
        );
      })
      .flatMap((key) =>
        imgs[parseInt(key, 10)].map((img) => ({
          value: parseInt(key, 10),
          label: `Paciente: ${img.patientName}, Serie: ${img.seriesDescription}, Modalidad: ${img.modality}, Fecha: ${img.studyDate}`,
        }))
      );
  }, [imgs, imgSets, selectedPatient, selectedSeries, selectedModality, selectedStudyDate]);
  const handleFileChange = async (files: FileList) => {
    for (const file of files) {
      const arrayBuffer = await file.arrayBuffer();
      const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
      const patientName = dataSet.string('x00100010') || 'Desconocido';
      const seriesDescription = dataSet.string('x0008103e') || 'No especificado';
      const modality = dataSet.string('x00080060') || 'No especificado';
      const studyDate = dataSet.string('x00080020') || 'Sin fecha';
      const [, imgSet, imgId] = file.name
        .split('.')[0]
        .split('-')
        .map((s) => parseInt(s, 10));
      if (!imgSets[imgSet]) {
        setImgSets((prev) => {
          const newImgSets = [...prev];
          newImgSets[imgSet] = [];
          return newImgSets;
        });
      }
      const imageId: string = cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
      setImgs((prev) => {
        const updatedImgs = {
          ...prev,
          [imgSet]: prev[imgSet]
            ? [...prev[imgSet], {
                imageId,
                instanceNumber: imgId,
                patientName,
                seriesDescription,
                modality,
                studyDate,
              }]
            : [{
                imageId,
                instanceNumber: imgId,
                patientName,
                seriesDescription,
                modality,
                studyDate,
              }]
        };
        console.log("Imágenes actualizadas:", updatedImgs);
        return updatedImgs;
      });
      setSelectedImageSets(prev => {
        const newSelectedImageSets = [...prev];
        // Encuentra el primer panel sin un set de imágenes asignado
        const firstEmptyPanelIndex = newSelectedImageSets.findIndex(set => set === null);
        if (firstEmptyPanelIndex !== -1) {
          newSelectedImageSets[firstEmptyPanelIndex] = imgSet;
        }
        return newSelectedImageSets;
      });
      const formData = new FormData();
      formData.append('image', file);
      try {
        const response = await axios.post('http://localhost:3000/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        });
        console.log('Respuesta del servidor:', response.data);
      } catch (error) {
        console.error('Error al enviar la imagen:', error);
      }
    }
  };
  const clearFilters = useCallback(() => {
    setSelectedPatient(null);
    setSelectedSeries(null);
    setSelectedModality(null);
    setSelectedStudyDate(null);
  }, []);
  return (
    <div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', flexDirection: 'row' }}>
        <input
          type="file"
          multiple
          onChange={(e) => e.target.files && void handleFileChange(e.target.files)}
        />
        <Select
          placeholder="Filtrar por paciente"
          value={selectedPatient ? { value: selectedPatient, label: selectedPatient } : null}
          options={filterOptions.patients}
          onChange={(option) => setSelectedPatient(option?.value ?? null)}
          isClearable
        />
        <Select
          placeholder="Filtrar por serie"
          value={selectedSeries ? { value: selectedSeries, label: selectedSeries } : null}
          options={filterOptions.series}
          onChange={(option) => setSelectedSeries(option?.value ?? null)}
          isClearable
        />
        <Select
          placeholder="Filtrar por modalidad"
          value={selectedModality ? { value: selectedModality, label: selectedModality } : null}
          options={filterOptions.modalities}
          onChange={(option) => setSelectedModality(option?.value ?? null)}
          isClearable
        />
        <Select
          placeholder="Filtrar por fecha"
          value={selectedStudyDate ? { value: selectedStudyDate, label: selectedStudyDate } : null}
          options={filterOptions.dates}
          onChange={(option) => setSelectedStudyDate(option?.value ?? null)}
          isClearable
        />
        <button 
          onClick={clearFilters}
          style={{
            padding: '8px 16px',
            backgroundColor: '#f44336',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Limpiar Filtros
        </button>
      </div>
      <DropInput
        key={columns * rows}
        onDrop={(files) => void handleFileChange(files)}
        borderColor="black"
        onDragOverColor="blue"
        style={{
          height: '90%',
          width: '100%',
          display: 'flex',
          flexDirection: 'row',
        }}
      >
        <div style={{ display: 'grid', width: '100%', height: '100%', backgroundColor: 'black', gridTemplateColumns: `repeat(${columns.toString()}, 1fr)`, gridTemplateRows: `repeat(${rows.toString()}, 1fr)` }}>
          {Array.from({ length: columns * rows }).map((_, i) => (
            <div key={i} style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
Do not use Array index in keys

              <Select
                value={selectedImageSets[i] ? { value: selectedImageSets[i], label: selectedImageSets[i].toString() } : null}
                styles={{ container: (provided) => ({ ...provided, width: '100%', margin: 0, padding: 0 }) }}
                options={Object.keys(imgSets)
                  .filter((key) => imgs[parseInt(key, 10)]?.length > 0)
                  .flatMap((key) =>
                    imgs[parseInt(key, 10)].map((img) => ({
                      value: parseInt(key, 10),
                      label: `Paciente: ${img.patientName}, Serie: ${img.seriesDescription}, Modalidad: ${img.modality}, Fecha: ${img.studyDate}`,
                    }))
                  )}
                onChange={(selected) => {
                  setSelectedImageSets((prev) => {
                    const newSelectedSets = [...prev];
                    newSelectedSets[i] = selected?.value ?? null;
                    return newSelectedSets;
                  });
                }}
                getOptionLabel={(option) => option.label}
                getOptionValue={(option) => option.value}
              />
              <Panel
                key={i}
                imageIds={selectedImageSets[i] ? imgs[selectedImageSets[i]].map((img) => img.imageId) : []}
                style={{ width: '100%', height: '100%', backgroundColor: 'darkgray', borderWidth: '1px', borderStyle: 'solid', borderRadius: '5px' }}
              />
            </div>
          ))}
        </div>
      </DropInput>
    </div>
  );
};
export default PanelGroup;

Clone this wiki locally