Numerosos errores tipográficos y código Copy-Paste se convirtieron en el tema principal del artículo adicional sobre la verificación del código Haiku por el analizador PVS-Studio. Sin embargo, este artículo trata principalmente sobre errores relacionados con la falta de consideración y la refactorización fallida, en lugar de errores tipográficos. Los errores encontrados demuestran cuán fuerte es el factor humano en el desarrollo de software.
Introduccion
Haiku es un sistema operativo gratuito de código abierto para computadoras personales. Un equipo de desarrollo internacional está trabajando actualmente en los componentes del sistema. Portar Libre Office en el sistema operativo y la primera versión R1 Beta 1 se destacan entre las recientes mejoras significativas de desarrollo.
El equipo de desarrolladores de
PVS-Studio sigue el desarrollo de este proyecto desde 2015 y publica revisiones de defectos de código. Esta es la cuarta revisión de todos los tiempos. Puede leer los artículos anteriores en estos enlaces:
- Análisis del sistema operativo Haiku (Familia BeOS), por PVS-Studio, Parte 1 ;
- Análisis del sistema operativo Haiku (familia BeOS) por PVS-Studio. Parte 2 ;
- Cómo dispararte en el pie en C y C ++. Libro de cocina Haiku OS
La característica del último análisis de código es la capacidad de usar la versión oficial de PVS-Studio para Linux. Ni PVS-Studio para Linux, ni un informe conveniente para ver los errores estaban disponibles en 2015. Esta vez enviaremos el informe completo en un formato conveniente a los desarrolladores de Haiku.
Clásico
V501 Hay
subexpresiones idénticas a la izquierda y a la derecha del operador '-': (addr_t) b - (addr_t) b BitmapManager.cpp 51
int compare_app_pointer(const ServerApp* a, const ServerApp* b) { return (addr_t)b - (addr_t)b; }
Cada desarrollador tiene que mezclar las variables
a y
b ,
x e
y ,
i y
j ... al menos una vez en su vida.
V501 Hay
subexpresiones idénticas a la izquierda y a la derecha de '||' operador: input == __null || input == __null MediaClient.cpp 182
status_t BMediaClient::Unbind(BMediaInput* input, BMediaOutput* output) { CALLED(); if (input == NULL || input == NULL) return B_ERROR; if (input->fOwner != this || output->fOwner != this) return B_ERROR; input->fBind = NULL; output->fBind = NULL; return B_OK; }
El mismo puntero de
entrada se verifica en la condición dos veces. Mientras que el puntero de
salida permaneció sin marcar, lo que puede dar como resultado la anulación de la referencia del puntero.
Código fijo:
if (input == NULL || output == NULL) return B_ERROR;
V583 El
operador '?:', Independientemente de su expresión condicional, siempre devuelve el mismo valor: 500000. usb_modeswitch.cpp 361
static status_t my_transfer_data(....) { .... do { bigtime_t timeout = directionIn ? 500000 : 500000; result = acquire_sem_etc(device->notify, 1, B_RELATIVE_TIMEOUT, timeout); .... } while (result == B_INTERRUPTED); .... }
El operador ternario se volvió inútil, cuando el autor del código cometió un error y escribió dos valores de retorno idénticos:
500000 .
V519 A la variable 'm_kindex1' se le asignan valores dos veces seguidas. Quizás esto sea un error. Verifique las líneas: 40, 41. agg_trans_double_path.cpp 41
trans_double_path::trans_double_path() : m_kindex1(0.0), m_kindex2(0.0), m_base_length(0.0), m_base_height(1.0), m_status1(initial), m_status2(initial), m_preserve_x_scale(true) { } void trans_double_path::reset() { m_src_vertices1.remove_all(); m_src_vertices2.remove_all(); m_kindex1 = 0.0; m_kindex1 = 0.0; m_status1 = initial; m_status2 = initial; }
Hay un error en la función de
reinicio : un error tipográfico en el índice variable
m_kindex2 . Esta variable no se restablecerá, lo que probablemente afectará la ejecución de otros fragmentos de código.
V501 Hay
subexpresiones idénticas a la izquierda y a la derecha del operador '>': fg [order_type :: R]> fg [order_type :: R] agg_span_image_filter_rgba.h 898
typedef Source source_type; typedef typename source_type::color_type color_type; typedef typename source_type::order_type order_type; void generate(color_type* span, int x, int y, unsigned len) { .... if(fg[0] < 0) fg[0] = 0; if(fg[1] < 0) fg[1] = 0; if(fg[2] < 0) fg[2] = 0; if(fg[3] < 0) fg[3] = 0; if(fg[order_type::A] > base_mask) fg[order_type::A] = base_mask; if(fg[order_type::R] > fg[order_type::R])fg[order_type::R] = fg[order_type::R]; if(fg[order_type::G] > fg[order_type::G])fg[order_type::G] = fg[order_type::G]; if(fg[order_type::B] > fg[order_type::B])fg[order_type::B] = fg[order_type::B]; .... }
En las últimas líneas, hay dos problemas a la vez: comparación y asignación de variables iguales. Ni siquiera puedo sugerir cuál fue la creación del autor. Solo notaré este fragmento como sospechoso.
V570 La variable 'wPipeIndex' se asigna a sí misma. CEchoGals_transport.cpp 244
ECHOSTATUS CEchoGals::CloseAudio (....) { .... wPipeIndex = wPipeIndex; m_ProcessId[ wPipeIndex ] = NULL; m_Pipes[ wPipeIndex ].wInterleave = 0; .... }
La variable
wPipeIndex se inicializa por su propio valor. Lo más probable es que se haya cometido un error tipográfico.
Errores con punteros
V522 Puede tener lugar la desreferenciación del puntero nulo 'currentInterface'. Device.cpp 258
Device::Device(....) : .... { .... usb_interface_info* currentInterface = NULL;
El puntero
currentInterface se inicializa inicialmente por nulo y luego se verifica al ingresar en las ramas del operador del
interruptor , pero no en todos los casos. El analizador advierte que al saltar a la
etiqueta de caso
USB_DESCRIPTOR_ENDPOINT_COMPANION , puede producirse una
anulación de puntero nulo.
V522 Puede tener lugar la desreferenciación del puntero nulo 'directorio'. PathMonitor.cpp 1465
bool PathHandler::_EntryCreated(....) { .... Directory* directory = directoryNode->ToDirectory(); if (directory == NULL) {
Creo que hay un error en la condición de comparación del puntero del
directorio con el valor nulo; La condición tiene que ser lo contrario. Con la implementación actual, si la variable
dryRun es
falsa , el puntero nulo del
directorio será desreferenciado.
V522 Puede tener lugar la desreferenciación del puntero nulo 'input'. MediaRecorder.cpp 343
void GetInput(media_input* input); const media_input& BMediaRecorder::MediaInput() const { CALLED(); media_input* input = NULL; fNode->GetInput(input); return *input; }
El puntero de
entrada se inicializa por nulo y permanece con dicho valor, ya que el puntero no cambia en la función GetInput. En otros métodos de la clase
BMediaRecorder , la implementación es diferente, por ejemplo:
status_t BMediaRecorder::_Connect(....) { ....
Aquí todo es correcto, pero el primer fragmento debe reescribirse, de lo contrario la función devolverá una referencia a un objeto local.
V522 Puede tener lugar la desreferenciación del puntero nulo 'mustFree'. RequestUnflattener.cpp 35
status_t Reader::Read(int32 size, void** buffer, bool* mustFree) { if (size < 0 || !buffer || mustFree)
En la expresión condicional donde se verifican todos los datos incorrectos, el autor cometió un error tipográfico al verificar el puntero
mustFree . Lo más probable es que la función salga cuando tenga el valor nulo de este puntero:
if (size < 0 || !buffer || !mustFree)
V757 Es posible que una variable incorrecta se compare con nullptr después de la conversión de tipo usando 'dynamic_cast'. Líneas de verificación: 474, 476. recover.cpp 474
void checkStructure(Disk &disk) { .... Inode* missing = gMissing.Get(run); dir = dynamic_cast<Directory *>(missing); if (missing == NULL) { .... } .... }
El desarrollador debería haber verificado el puntero
dir en lugar de
faltar después de la conversión de tipo. Por cierto, los desarrolladores de C # a
menudo también cometen un error similar. Esto prueba una vez más que algunos errores no dependen del idioma utilizado.
Un par de lugares más similares en el código:
- V757 Es posible que una variable incorrecta se compare con nullptr después de la conversión de tipo usando 'dynamic_cast'. Líneas de verificación: 355, 357. ExpandoMenuBar.cpp 355
- V757 Es posible que una variable incorrecta se compare con nullptr después de la conversión de tipo usando 'dynamic_cast'. Líneas de verificación: 600, 601. ValControl.cpp 600
Errores de índice
V557 Array overrun es posible. El índice 'BT_SCO' apunta más allá de la matriz. h2upper.cpp 75
struct bt_usb_dev { .... struct list nbuffersTx[(1 + 1 + 0 + 0)];
La matriz
bdev-> nbuffersTx consta solo de 2 elementos, pero está dirigida por la constante BT_SCO, que es 3. Aquí viene el índice de la matriz segura fuera de límites.
V557 Array overrun es posible. La función 'ieee80211_send_setup' procesa el valor '16'. Inspeccione el cuarto argumento. Verifique las líneas: 842, 911. ieee80211_output.c 842
struct ieee80211_node { .... struct ieee80211_tx_ampdu ni_tx_ampdu[16];
Otro índice de matriz fuera de los límites. Esta vez, solo por un elemento. El análisis interprocedural ayudó a revelar el caso cuando la matriz
ni-> ni_tx_ampdu , que consta de 16 elementos, fue abordada por el índice 16. En C y C ++ las matrices se indexan desde cero.
V781 El valor de la variable 'vector' se verifica después de su uso. Quizás haya un error en la lógica del programa. Líneas de verificación: 802, 805. oce_if.c 802
#define OCE_MAX_EQ 32 typedef struct oce_softc { .... OCE_INTR_INFO intrs[OCE_MAX_EQ]; .... } OCE_SOFTC, *POCE_SOFTC; static int oce_alloc_intr(POCE_SOFTC sc, int vector, void (*isr) (void *arg, int pending)) { POCE_INTR_INFO ii = &sc->intrs[vector]; int rc = 0, rr; if (vector >= OCE_MAX_EQ) return (EINVAL); .... }
El analizador ha detectado que un elemento de la matriz
sc-> intrs fue abordado por un índice no válido, que estaba fuera de los límites. La razón es el orden incorrecto de las operaciones en el código. Primero, se direcciona el elemento y luego viene la verificación de si el valor del índice es válido.
Algunos podrían decir que no habrá ningún problema. No elimina el valor del elemento de matriz, solo toma la dirección de la celda. Pero no, esa no es la forma de hacer las cosas. Leer más: "La
desreferencia de puntero nulo provoca un comportamiento indefinido ".
V519 A la variable se le asignan valores dos veces seguidas. Quizás esto sea un error. Líneas de verificación: 199, 200. nvme_ctrlr.c 200
static void nvme_ctrlr_set_intel_supported_features(struct nvme_ctrlr *ctrlr) { bool *supported_feature = ctrlr->feature_supported; supported_feature[NVME_INTEL_FEAT_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_NATIVE_MAX_LBA] = true; supported_feature[NVME_INTEL_FEAT_POWER_GOVERNOR_SETTING] = true; supported_feature[NVME_INTEL_FEAT_SMBUS_ADDRESS] = true; supported_feature[NVME_INTEL_FEAT_LED_PATTERN] = true; supported_feature[NVME_INTEL_FEAT_RESET_TIMED_WORKLOAD_COUNTERS] = true; supported_feature[NVME_INTEL_FEAT_LATENCY_TRACKING] = true; }
Al elemento de matriz con el índice
NVME_INTEL_FEAT_MAX_LBA se le asigna el mismo valor. La buena noticia es que esta función presenta todas las constantes posibles, lo que hace que este código sea solo el resultado de la programación Copy-Paste. Pero es probable que los errores se cuelen aquí.
V519 La
variable 'copiedPath [len]' tiene valores asignados dos veces sucesivamente. Quizás esto sea un error. Líneas de verificación: 92, 93. kernel_emu.cpp 93
int UserlandFS::KernelEmu::new_path(const char *path, char **copy) { ....
Bueno, aquí el programador tuvo mala suerte con la copia. El símbolo "punto" se agrega a una línea y se reescribe con un terminal nulo. Es muy probable que el autor haya copiado la línea y se haya olvidado de incrementar el índice.
Condiciones extrañas
V517 El uso del
patrón 'if (A) {...} else if (A) {...}' fue detectado. Hay una probabilidad de presencia de error lógico. Líneas de verificación: 1407, 1410. FindPanel.cpp 1407
void FindPanel::BuildAttrQuery(BQuery* query, bool &dynamicDate) const { .... case B_BOOL_TYPE: { uint32 value; if (strcasecmp(textControl->Text(), "true") == 0) { value = 1; } else if (strcasecmp(textControl->Text(), "true") == 0) { value = 1; } else value = (uint32)atoi(textControl->Text()); value %= 2; query->PushUInt32(value); break; } .... }
Copiar el código condujo a dos errores a la vez. Las expresiones condicionales son idénticas. Lo más probable es que una comparación con la cadena "falsa" en lugar de "verdadero" tenga que estar en una de ellas. Además en la rama que maneja el valor "falso", el
valor que debe cambiarse de
1 a
0 . El algoritmo requiere que cualquier otro valor, diferente de
verdadero o
falso, se convierta en un número utilizando la función
atoi . Pero debido a un error, el texto "falso" entrará en la función.
V547 La expresión 'error == ((int) 0)' siempre es verdadera. Directory.cpp 688
int32 BDirectory::CountEntries() { status_t error = Rewind(); if (error != B_OK) return error; int32 count = 0; BPrivate::Storage::LongDirEntry entry; while (error == B_OK) { if (GetNextDirents(&entry, sizeof(entry), 1) != 1) break; if (strcmp(entry.d_name, ".") != 0 && strcmp(entry.d_name, "..") != 0) count++; } Rewind(); return (error == B_OK ? count : error); }
El analizador detectó que el valor de la variable de
error siempre será
B_OK . Definitivamente, esta modificación variable se perdió en el ciclo while.
V564 El operador '&' se aplica al valor de tipo bool. Probablemente haya olvidado incluir paréntesis o tenga la intención de utilizar el operador '&&'. strtod.c 545
static int lo0bits(ULong *y) { int k; ULong x = *y; .... if (!(x & 1)) { k++; x >>= 1; if (!x & 1)
Es muy probable que en la última expresión condicional se haya olvidado colocar corchetes, como en las condiciones anteriores. Es probable que el operador complementario esté fuera de los corchetes:
if (!(x & 1))
V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. PoseView.cpp 5851
bool BPoseView::AttributeChanged(const BMessage* message) { .... result = poseModel->OpenNode(); if (result == B_OK || result != B_BUSY) break; .... }
Esto no es obvio, pero el resultado de la condición no depende del valor del valor B_OK. Entonces se puede simplificar:
If (result != B_BUSY) break;
Puede verificarlo fácilmente dibujando una tabla de verdad para los valores de la variable de
resultado . Si uno quisiera considerar específicamente otros valores, diferentes de
B_OK y
B_BUSY , el código debe reescribirse de otra manera.
Dos fragmentos más similares:
- V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. Tracker.cpp 1714
- V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. if_ipw.c 1871
V590 Considere inspeccionar el 'argc == 0 || argc! = expresión de 2 '. La expresión es excesiva o contiene un error de imprenta. cmds.c 2667
void unsetoption(int argc, char *argv[]) { .... if (argc == 0 || argc != 2) { fprintf(ttyout, "usage: %s option\n", argv[0]); return; } .... }
Este es quizás el ejemplo más simple que demuestra el trabajo del diagnóstico
V590 . Debe mostrar la descripción del programa en caso de que no haya argumentos aprobados o de que no haya dos. Obviamente, cualquier valor que no sea dos, incluido cero, no satisfará la condición. Por lo tanto, la condición se puede simplificar de manera segura a esto:
if (argc != 2) { fprintf(ttyout, "usage: %s option\n", argv[0]); return; }
V590 Considere inspeccionar el '* ptr =='; ' && * ptr! = '\ 0' 'expresión. La expresión es excesiva o contiene un error de imprenta. pc.c 316
ULONG parse_expression(char *str) { .... ptr = skipwhite(ptr); while (*ptr == SEMI_COLON && *ptr != '\0') { ptr++; if (*ptr == '\0') continue; val = assignment_expr(&ptr); } .... }
En este ejemplo, se cambió el operador lógico, pero la lógica sigue siendo la misma. Aquí la condición del ciclo while depende solo de si el carácter es igual a
SEMI_COLON o no.
V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. writembr.cpp 99
int main(int argc, char** argv) { .... string choice; getline(cin, choice, '\n'); if (choice == "no" || choice == "" || choice != "yes") { cerr << "MBR was NOT written" << endl; fs.close(); return B_ERROR; } .... }
Ya hay tres condiciones en este ejemplo. También se puede simplificar antes de verificar si el usuario ha elegido "sí" o no:
if (choice != "yes") { cerr << "MBR was NOT written" << endl; fs.close(); return B_ERROR; }
Misceláneo
V530 Se requiere utilizar el valor de retorno de la función 'comenzar'. IMAPFolder.cpp 414
void IMAPFolder::RegisterPendingBodies(...., const BMessenger* replyTo) { .... IMAP::MessageUIDList::const_iterator iterator = uids.begin(); for (; iterator != uids.end(); iterator++) { if (replyTo != NULL) fPendingBodies[*iterator].push_back(*replyTo); else fPendingBodies[*iterator].begin();
El analizador encontró una llamada sin sentido del iterador
begin (). No puedo imaginar cómo arreglar el código. Los desarrolladores deben prestar atención a este código.
V609 Divide por cero. Rango del denominador [0..64]. UiUtils.cpp 544
static int32 GetSIMDFormatByteSize(uint32 format) { switch (format) { case SIMD_RENDER_FORMAT_INT8: return sizeof(char); case SIMD_RENDER_FORMAT_INT16: return sizeof(int16); case SIMD_RENDER_FORMAT_INT32: return sizeof(int32); case SIMD_RENDER_FORMAT_INT64: return sizeof(int64); case SIMD_RENDER_FORMAT_FLOAT: return sizeof(float); case SIMD_RENDER_FORMAT_DOUBLE: return sizeof(double); } return 0; } const BString& UiUtils::FormatSIMDValue(const BVariant& value, uint32 bitSize, uint32 format, BString& _output) { _output.SetTo("{"); char* data = (char*)value.ToPointer(); uint32 count = bitSize / (GetSIMDFormatByteSize(format) * 8);
La función
GetSIMDFormatByteSize realmente devuelve
0 como valor predeterminado, lo que podría conducir a la división por cero.
V654 La condición '
SpecificSequence ! = Sequence' del bucle siempre es falsa. pthread_key.cpp 55
static void* get_key_value(pthread_thread* thread, uint32 key, int32 sequence) { pthread_key_data& keyData = thread->specific[key]; int32 specificSequence; void* value; do { specificSequence = keyData.sequence; if (specificSequence != sequence) return NULL; value = keyData.value; } while (specificSequence != sequence); keyData.value = NULL; return value; }
El analizador tiene razón en que la condición del operador
while siempre es falsa. Debido a esto, el bucle no ejecuta más de una iteración. En otras palabras, nada cambiaría si escribiera
while (0) . Todo esto es extraño y este código contiene un error lógico. Los desarrolladores deben considerar cuidadosamente este fragmento.
V672 Probablemente no sea necesario crear la nueva variable 'ruta' aquí. Uno de los argumentos de la función posee el mismo nombre y este argumento es una referencia. Líneas de verificación: 348, 429. translate.cpp 429
status_t Translator::FindPath(...., TypeList &path, double &pathQuality) { .... TypeList path; double quality; if (FindPath(&formats[j], stream, typesSeen, path, quality) == B_OK) { if (bestQuality < quality * formatQuality) { bestQuality = quality * formatQuality; bestPath.SetTo(path); bestPath.Add(formats[j].type); status = B_OK; } } .... }
La variable de
ruta se pasa a la función
FindPath por referencia. Lo que significa que esta variable se puede modificar en el cuerpo de la función. Pero hay una variable local con el mismo nombre, que se modifica. En este caso, todos los cambios permanecerán solo en la variable local. El autor del código puede querer cambiar el nombre o eliminar la variable local.
V705 Es posible que el bloque 'else' haya sido olvidado o comentado, alterando así las lógicas de operación del programa. HostnameView.cpp 109
status_t HostnameView::_LoadHostname() { BString fHostnameString; char hostname[MAXHOSTNAMELEN]; if (gethostname(hostname, MAXHOSTNAMELEN) == 0) { fHostnameString.SetTo(hostname, MAXHOSTNAMELEN); fHostname->SetText(fHostnameString); return B_OK; } else return B_ERROR; }
El ejemplo de formato de código deficiente. La palabra clave "colgar" no cambia la lógica todavía, pero una vez que se inserta un fragmento de código antes del operador de
retorno , la lógica no será la misma.
El 'menú' del parámetro
V763 siempre se reescribe en el cuerpo de la función antes de usarse. video.cpp 648
bool video_mode_hook(Menu *menu, MenuItem *item) { video_mode *mode = NULL; menu = item->Submenu(); item = menu->FindMarked(); .... }
Encontré muchos casos cuando los argumentos de la función se reescriben al ingresar a la función. Este comportamiento engaña a otros desarrolladores que llaman a estas funciones.
La lista completa de lugares sospechosos:
- V763 El parámetro 'force_16bit' siempre se reescribe en el cuerpo de la función antes de usarse. ata_adapter.cpp 151
- V763 El parámetro 'force_16bit' siempre se reescribe en el cuerpo de la función antes de usarse. ata_adapter.cpp 179
- El 'menú' del parámetro V763 siempre se reescribe en el cuerpo de la función antes de usarse. video.cpp 264
- V763 El parámetro 'length' siempre se reescribe en el cuerpo de la función antes de usarse. MailMessage.cpp 677
- V763 El parámetro 'entrada' siempre se reescribe en el cuerpo de la función antes de ser utilizado. IconCache.cpp 773
- V763 El parámetro 'entrada' siempre se reescribe en el cuerpo de la función antes de ser utilizado. IconCache.cpp 832
- V763 El parámetro 'entrada' siempre se reescribe en el cuerpo de la función antes de ser utilizado. IconCache.cpp 864
- V763 El parámetro 'rect' siempre se reescribe en el cuerpo de la función antes de usarse. ErrorLogWindow.cpp 56
- El parámetro V763 'updateRect' siempre se reescribe en el cuerpo de la función antes de usarse. CalendarMenuWindow.cpp 49
- V763 El parámetro 'rect' siempre se reescribe en el cuerpo de la función antes de usarse. MemoryView.cpp 165
- V763 El parámetro 'rect' siempre se reescribe en el cuerpo de la función antes de usarse. TypeEditors.cpp 1124
- V763 El parámetro 'altura' siempre se reescribe en el cuerpo de la función antes de usarse. Workspaces.cpp 857
- V763 El parámetro 'ancho' siempre se reescribe en el cuerpo de la función antes de usarse. Workspaces.cpp 856
- El parámetro 'frame' V763 siempre se reescribe en el cuerpo de la función antes de ser utilizado. SwatchGroup.cpp 48
- El parámetro 'frame' V763 siempre se reescribe en el cuerpo de la función antes de ser utilizado. PlaylistWindow.cpp 89
- V763 El parámetro 'rect' siempre se reescribe en el cuerpo de la función antes de usarse. ConfigView.cpp 78
- V763 El parámetro 'm' siempre se reescribe en el cuerpo de la función antes de usarse. mkntfs.c 3917
- El parámetro V763 'rxchainmask' siempre se reescribe en el cuerpo de la función antes de usarse. ar5416_cal.c 463
- V763 El parámetro 'c' siempre se reescribe en el cuerpo de la función antes de usarse. if_iwn.c 6854
Conclusión
El proyecto Haiku es una fuente de errores interesantes y raros. Agregamos a nuestra base de datos algunos ejemplos de errores y solucionamos algunos problemas del analizador que aparecían al analizar el código.
Si no ha verificado su código con algunas herramientas de análisis de código durante mucho tiempo, entonces algunos de los problemas que describí probablemente estén ocultos en su código. Use PVS-Studio en su proyecto (si está escrito en C, C ++, C # o Java) para controlar la calidad del código. Descargue el analizador
aquí sin registro o sms.
¿Quieres probar Haiku y tienes preguntas? Los desarrolladores de Haiku te invitan al
canal de telegramas .