Godot: sobre el tema del uso regular de analizadores de código estático

PVS-Studio y Godot Nuestros lectores están creciendo, por lo que escribimos artículos una y otra vez que explican cómo usar correctamente la metodología de análisis de código estático. Consideramos muy importante explicar que las herramientas de análisis estático no deben usarse esporádicamente, sino regularmente. Una vez más, demostramos esto con un ejemplo práctico, volviendo a verificar el proyecto Godot.

Use analizadores regularmente


Al prepararme para hablar en la conferencia de desarrolladores de juegos, decidí obtener nuevos ejemplos de errores interesantes que la herramienta PVS-Studio puede detectar. Para este propósito, se probaron varios motores de juego, uno de los cuales fue Godot. No encontré ningún error particularmente interesante para el informe, pero quería escribir un artículo sobre errores comunes. Estos errores demuestran muy bien la relevancia del uso regular de herramientas de análisis de código estático.

Cabe señalar que ya verificamos este proyecto en 2015, y el autor trabajó con los errores que escribimos. Aquí puedes ver el commit correspondiente.

Han pasado 3 años. El proyecto ha cambiado. El analizador PVS-Studio también ha cambiado y han aparecido nuevos diagnósticos. Por lo tanto, no es sorprendente que haya podido escribir fácil y rápidamente suficientes ejemplos de errores para escribir este artículo.

Sin embargo, algo más es importante. Al desarrollar Godot o cualquier otro proyecto, constantemente aparecen nuevos errores y se corrigen. Los errores no detectados "se asientan" en el código durante mucho tiempo, y luego se pueden detectar muchos de ellos al ejecutar el análisis de código estático. Debido a esto, a veces existe la falsa sensación de que los analizadores estáticos encuentran solo algunos errores poco interesantes en secciones de código poco utilizadas. Por supuesto, este es el caso si usa el analizador incorrectamente y lo ejecuta solo de vez en cuando, por ejemplo, poco antes del lanzamiento de la versión.

Sí, al escribir artículos, nosotros mismos realizamos verificaciones únicas de proyectos abiertos. Pero tenemos un objetivo diferente. Queremos demostrar las capacidades de un analizador de código para detectar defectos. Esta tarea tiene poco que ver con mejorar la calidad del código del proyecto en su conjunto y reducir los costos asociados con la corrección de errores.

Una vez más sobre lo principal. El objetivo del análisis de código estático no es encontrar errores antiguos. Estos viejos errores suelen ser insignificantes, de lo contrario habrían interferido con los usuarios y ya se habrían encontrado y corregido. El punto del análisis estático es corregir rápidamente los errores en el código recién escrito o modificado. Esto reduce el tiempo de depuración, el número de quejas de los usuarios y, en última instancia, reduce el presupuesto del proyecto desarrollado.

Ahora pasemos a los errores que los lectores de nuestras publicaciones adoran tanto.

Copiar y pegar errores


Veamos lo que noté al estudiar el informe PVS-Studio. Comenzaré con mi diagnóstico V501 favorito, que encuentra errores en casi todos los proyectos que verificamos :).

Error N1

virtual bool can_export(....) { .... if (!exists_export_template("uwp_" + platform_infix + "_debug.zip", &err) || !exists_export_template("uwp_" + platform_infix + "_debug.zip", &err)) { valid = false; r_missing_templates = true; } .... } 

Advertencia de PVS-Studio: V501 CWE-570 Hay subexpresiones idénticas '! Exists_export_template ("uwp_" + platform_infix + "_debug.zip", & err)' a la izquierda y a la derecha de '||' operador export.cpp 1135

Error clásico de copiar y pegar. La llamada de función se ha copiado pero no editado. El nombre del segundo archivo a procesar debe terminar con "_release.zip".

Errores N2, N3

 static String dump_node_code(SL::Node *p_node, int p_level) { .... if (bnode->statements[i]->type == SL::Node::TYPE_CONTROL_FLOW || bnode->statements[i]->type == SL::Node::TYPE_CONTROL_FLOW) { code += scode; //use directly } else { code += _mktab(p_level) + scode + ";\n"; } .... } 

Advertencia de PVS-Studio: V501 CWE-570 Hay subexpresiones idénticas 'bnode-> declaraciones [i] -> type == SL :: Node :: TYPE_CONTROL_FLOW' a la izquierda y a la derecha de '||' operador test_shader_lang.cpp 183

 void EditorSpinSlider::_notification(int p_what) { if (p_what == MainLoop::NOTIFICATION_WM_FOCUS_OUT || p_what == MainLoop::NOTIFICATION_WM_FOCUS_OUT) { if (grabbing_spinner) { Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); grabbing_spinner = false; grabbing_spinner_attempt = false; } } .... } 

Advertencia de PVS-Studio: V501 CWE-570 Hay subexpresiones idénticas 'p_what == MainLoop :: NOTIFICATION_WM_FOCUS_OUT' a la izquierda y a la derecha de '||' operador editor_spin_slider.cpp 157

Creo que los errores son claramente visibles y no requieren ninguna explicación. Exactamente el mismo clásico Copy-Paste, como en el primer caso.

Error N4

 String SoftBody::get_configuration_warning() const { .... Transform t = get_transform(); if ((ABS(t.basis.get_axis(0).length() - 1.0) > 0.05 || ABS(t.basis.get_axis(1).length() - 1.0) > 0.05 || ABS(t.basis.get_axis(0).length() - 1.0) > 0.05)) { if (!warning.empty()) .... } 

Advertencia de PVS-Studio: V501 CWE-570 Hay subexpresiones idénticas a la izquierda y a la derecha de '||' operador soft_body.cpp 399

Aquí la primera línea se copió dos veces. Pero el número del eje de coordenadas se cambió solo en la segunda línea. Y se olvidaron de editar la tercera línea. Esto no es más que el " Efecto de última línea ".

Nota Por el momento, además del "Efecto de última línea", he hecho las siguientes observaciones interesantes: " La función más peligrosa en el mundo C / C ++ ", "El mal vive en funciones de comparación ". Y ahora haré el anuncio de un nuevo artículo, cuya redacción planeo hacer en un futuro cercano. El título de trabajo es "0, 1, 2". Debería ser interesante e instructivo. Los invito a suscribirse a uno de los canales para no perderse: twitter , vk.com , Instagram , telegram y rss "old school".

Error N5

 void ScrollContainer::_notification(int p_what) { .... if (h_scroll->is_visible_in_tree() && h_scroll->get_parent() == this) size.y -= h_scroll->get_minimum_size().y; if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) size.x -= h_scroll->get_minimum_size().x; .... } 

Advertencia de PVS-Studio: V778 CWE-682 Se encontraron dos fragmentos de código similares. Quizás, este es un error tipográfico y se debe usar la variable 'v_scroll' en lugar de 'h_scroll'. scroll_container.cpp 249

Con respecto a este fragmento de código, no estoy completamente seguro de que haya un error. Sin embargo, estoy de acuerdo con el analizador en que el segundo bloque parece muy sospechoso. Lo más probable es que el código fue escrito usando Copy-Paste, y en el segundo bloque de texto se olvidaron de reemplazar h_scroll con v_scroll .

El código probablemente debería ser así:

 if (h_scroll->is_visible_in_tree() && h_scroll->get_parent() == this) size.y -= h_scroll->get_minimum_size().y; if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) size.x -= v_scroll->get_minimum_size().x; 

Error N6

Otro caso en el que se copió un fragmento de código lo suficientemente grande y se modificó sin éxito. La línea de error está marcada por mi comentario "// <=".

 void ShaderGLES2::bind_uniforms() { .... const Map<uint32_t, Variant>::Element *E = uniform_defaults.front(); while (E) { int idx = E->key(); int location = version->uniform_location[idx]; if (location < 0) { E = E->next(); continue; } Variant v; v = E->value(); _set_uniform_variant(location, v); E = E->next(); } const Map<uint32_t, CameraMatrix>::Element *C = uniform_cameras.front(); while (C) { int idx = E->key(); // <= int location = version->uniform_location[idx]; if (location < 0) { C = C->next(); continue; } glUniformMatrix4fv(location, 1, GL_FALSE, &(C->get().matrix[0][0])); C = C->next(); } uniforms_dirty = false; } 

Advertencia de PVS-Studio: V522 CWE-476 Puede producirse una desreferenciación del puntero nulo 'E'. shader_gles2.cpp 102

El error se detectó indirectamente. Gracias al análisis del flujo de datos, PVS-Studio reveló que el puntero E puede ser cero en el momento de la desreferenciación.

El error es que en el fragmento de código copiado se olvidaron de reemplazar E en C en un solo lugar. Debido a este error, la función funciona de una manera muy extraña y hace cosas extrañas.

Errores tipográficos


Error N7

Es difícil para los programadores que escriben en lenguajes distintos de C o C ++ imaginar que se puede hacer un error tipográfico escribiendo una coma en lugar de un asterisco *, y se compilará el código. Sin embargo, esto es así.

 LRESULT OS_Windows::WndProc(....) { .... BITMAPINFO bmi; ZeroMemory(&bmi, sizeof(BITMAPINFO)); bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = dib_size.x; bmi.bmiHeader.biHeight = dib_size.y; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; bmi.bmiHeader.biSizeImage = dib_size.x, dib_size.y * 4; .... } 

Advertencia de PVS-Studio: V521 CWE-480 Estas expresiones que usan el operador ',' son peligrosas. Asegúrate de que la expresión sea correcta. os_windows.cpp 776

A la variable bmi.bmiHeader.biSizeImage se le asigna el valor de la variable dib_size.x . A continuación, se ejecuta el operador de coma ',', cuya prioridad es menor que la del operador '='. El resultado de la expresión dib_size.y * 4 no se utiliza de ninguna manera.

En lugar de una coma en la expresión, se debe usar el operador de multiplicación '*'. En primer lugar, tal expresión tendría sentido. En segundo lugar, a continuación hay una opción similar, pero ya correcta, para inicializar la misma variable:

 bmi.bmiHeader.biSizeImage = dib_size.x * dib_size.y * 4; 

Errores N8, N9

 void Variant::set(....) { .... int idx = p_index; if (idx < 0) idx += 4; if (idx >= 0 || idx < 4) { Color *v = reinterpret_cast<Color *>(_data._mem); (*v)[idx] = p_value; valid = true; } .... } 

Advertencia de PVS-Studio: V547 CWE-571 Expresión 'idx> = 0 || idx <4 'siempre es cierto. variant_op.cpp 2152

Cualquier índice se considerará correcto. Para corregir el error, debe reemplazar el || en && :

 if (idx >= 0 && idx < 4) { 

Este error lógico probablemente surgió debido al descuido, por lo tanto, me inclino a atribuirlo a errores tipográficos.

Exactamente el mismo error se puede observar en el mismo archivo a continuación. La culpa del error de reproducción, aparentemente, es copiar y pegar.

Error múltiple: V547 CWE-571 Expresión 'idx> = 0 || idx <4 'siempre es cierto. variant_op.cpp 2527

Error N10

WTF?

Un ejemplo de un error del que uno quiere exclamar: ¡¿WTF?!

 void AnimationNodeBlendSpace1D::add_blend_point( const Ref<AnimationRootNode> &p_node, float p_position, int p_at_index) { ERR_FAIL_COND(blend_points_used >= MAX_BLEND_POINTS); ERR_FAIL_COND(p_node.is_null()); ERR_FAIL_COND(p_at_index < -1 || p_at_index > blend_points_used); if (p_at_index == -1 || p_at_index == blend_points_used) { p_at_index = blend_points_used; } else { for (int i = blend_points_used - 1; i > p_at_index; i++) { blend_points[i] = blend_points[i - 1]; } } .... } 

Advertencia de PVS-Studio: V621 CWE-835 Considere inspeccionar el operador 'para'. Es posible que el bucle se ejecute incorrectamente o no se ejecute en absoluto. animation_blend_space_1d.cpp 113

Tenga en cuenta la condición de detención del bucle: i> p_at_index . Siempre es cierto, ya que la variable i se inicializa con el valor blend_points_used - 1 . Además, de dos comprobaciones anteriores se deduce que blend_points_used> p_at_index .

La condición puede volverse falsa solo cuando se produce un desbordamiento de la variable de signo i , que es un comportamiento indefinido. Además, no alcanzará el desbordamiento, ya que la matriz irá más allá del límite mucho antes.

Ante nosotros, en mi opinión, es un error tipográfico hermoso cuando escribieron '>' en lugar de '<'. Sí, tengo una visión pervertida de la belleza de los errores :).

El ciclo correcto:

 for (int i = blend_points_used - 1; i < p_at_index; i++) { 

Error N11

Otro caso igualmente brillante de un error tipográfico en la condición del ciclo.

 void AnimationNodeStateMachineEditor::_state_machine_pos_draw() { .... int idx = -1; for (int i = 0; node_rects.size(); i++) { if (node_rects[i].node_name == playback->get_current_node()) { idx = i; break; } } .... } 

Advertencia de PVS-Studio: V693 CWE-835 Considere inspeccionar la expresión condicional del bucle. Es posible que se use 'i <X.size ()' en lugar de 'X.size ()'. animation_state_machine_editor.cpp 852

Puede producirse un desbordamiento de la matriz, ya que el valor de i aumenta sin control. Código seguro:

 for (int i = 0; i < node_rects.size(); i++) { 

Error N12

 GDScriptDataType GDScriptCompiler::_gdtype_from_datatype( const GDScriptParser::DataType &p_datatype) const { .... switch (p_datatype.kind) { .... case GDScriptParser::DataType::NATIVE: { result.kind = GDScriptDataType::NATIVE; result.native_type = p_datatype.native_type; } break; case GDScriptParser::DataType::SCRIPT: { result.kind = GDScriptDataType::SCRIPT; result.script_type = p_datatype.script_type; result.native_type = result.script_type->get_instance_base_type(); } case GDScriptParser::DataType::GDSCRIPT: { result.kind = GDScriptDataType::GDSCRIPT; result.script_type = p_datatype.script_type; result.native_type = result.script_type->get_instance_base_type(); } break; .... } 

Advertencia de PVS-Studio: V796 CWE-484 Es posible que falte la declaración 'break' en la declaración del interruptor. gdscript_compiler.cpp 135

Accidentalmente olvidé escribir una declaración de descanso . Por lo tanto, cuando entra en el caso GDScriptParser :: DataType :: SCRIPT, a las variables se les asignarán valores, como si fuera el caso GDScriptParser :: DataType :: GDSCRIPT .

Error N13

El siguiente error se puede clasificar como Copiar-Pegar. Sin embargo, no estoy seguro de si se copió una línea tan corta. Por lo tanto, consideraremos que esto es un error tipográfico simple al escribir.

 void CPUParticles::_particles_process(float p_delta) { .... if (flags[FLAG_DISABLE_Z]) { p.velocity.z = 0.0; p.velocity.z = 0.0; } .... } 

Advertencia de PVS-Studio: V519 CWE-563 La variable 'p.velocity.z' recibe valores asignados dos veces sucesivamente. Quizás esto sea un error. Líneas de verificación: 664, 665. cpu_particles.cpp 665

Dos veces la asignación de la misma variable. A continuación puede ver el siguiente fragmento de código:

 if (flags[FLAG_DISABLE_Z]) { p.velocity.z = 0.0; p.transform.origin.z = 0.0; } 

Lo más probable es que, para el primer caso, debería haber sido escrito de la misma manera.

Error N14

 bool AtlasTexture::is_pixel_opaque(int p_x, int p_y) const { if (atlas.is_valid()) { return atlas->is_pixel_opaque( p_x + region.position.x + margin.position.x, p_x + region.position.y + margin.position.y ); } return true; } 

Advertencia de PVS-Studio: el parámetro V751 'p_y' no se usa dentro del cuerpo de la función. texture.cpp 1085

Fragmento de la descripción del diagnóstico V751 :

El analizador detectó una función sospechosa, uno de cuyos parámetros nunca se utiliza. Al mismo tiempo, su otro parámetro se usa varias veces, lo que, posiblemente, indica la presencia de un error.

Como puede ver, esto es realmente así, y es muy sospechoso. La variable p_x se usa dos veces, y p_y no se usa. Lo más probable es que deba escribirse:

 return atlas->is_pixel_opaque( p_x + region.position.x + margin.position.x, p_y + region.position.y + margin.position.y ); 

Por cierto, en el código fuente, la llamada a la función se escribe en una línea. Debido a esto, el error es más difícil de notar. Si el autor del código escribió los argumentos reales en una columna, como hice en el artículo, entonces el error inmediatamente me llamaría la atención. Recuerde que el formato de tabla es muy útil y evita muchos errores tipográficos. Consulte el capítulo "Alinee el mismo tipo de código con una" tabla "en el artículo" El tema principal de la programación, refactorización y todo eso ".

Error N15

 bool SpriteFramesEditor::can_drop_data_fw(....) const { .... Vector<String> files = d["files"]; if (files.size() == 0) return false; for (int i = 0; i < files.size(); i++) { String file = files[0]; String ftype = EditorFileSystem::get_singleton()->get_file_type(file); if (!ClassDB::is_parent_class(ftype, "Texture")) { return false; } } .... } 

Advertencia de PVS-Studio: V767 Acceso sospechoso al elemento de la matriz de 'archivos' por un índice constante dentro de un bucle. sprite_frames_editor_plugin.cpp 602

El bucle procesa el mismo archivo en todas las iteraciones del bucle. Error tipográfico aquí:

 String file = files[0]; 

Debe ser:

 String file = files[i]; 

Otros errores


Error N16

 CSGBrush *CSGBox::_build_brush() { .... for (int i = 0; i < 6; i++) { .... if (i < 3) face_points[j][(i + k) % 3] = v[k] * (i >= 3 ? -1 : 1); else face_points[3 - j][(i + k) % 3] = v[k] * (i >= 3 ? -1 : 1); .... } .... } 

El analizador PVS-Studio produce inmediatamente dos respuestas a este código:

  • V547 CWE-570 La expresión 'i> = 3' siempre es falsa. csg_shape.cpp 939
  • V547 CWE-571 La expresión 'i> = 3' siempre es verdadera. csg_shape.cpp 941

De hecho, este operador ternario en ambas expresiones se ve muy extraño:

 i >= 3 ? -1 : 1 

En un caso, la condición siempre es verdadera, y en el otro es falsa. Es difícil decir cómo este código debería verse bien. Quizás es simplemente redundante y se puede escribir así:

 for (int i = 0; i < 6; i++) { .... if (i < 3) face_points[j][(i + k) % 3] = v[k]; else face_points[3 - j][(i + k) % 3] = -v[k]; .... } 

Puedo estar equivocado, y el código necesita ser reparado de una manera completamente diferente.

Error N17

Casi no hubo errores como V595, aunque generalmente se encuentran en abundancia en cualquier proyecto . Aparentemente, estos errores se corrigieron después de una verificación previa, y luego los errores de este tipo casi no aparecieron. Solo vi unos pocos falsos positivos y un error.

 bool CanvasItemEditor::_get_bone_shape(....) { .... Node2D *from_node = Object::cast_to<Node2D>( ObjectDB::get_instance(bone->key().from)); .... if (!from_node->is_inside_tree()) return false; //may have been removed if (!from_node) return false; .... } 

Advertencia de PVS-Studio: V595 CWE-476 El puntero 'from_node' se utilizó antes de verificarlo con nullptr. Verifique las líneas: 565, 567. canvas_item_editor_plugin.cpp 565

El puntero from_node primero se desreferencia para llamar a la función is_inside_tree, y solo entonces se verifica la igualdad nullptr . Los cheques deben intercambiarse:

 if (!from_node) return false; if (!from_node->is_inside_tree()) return false; //may have been removed 

Error N18
 enum JoystickList { .... JOY_AXIS_MAX = 10, .... }; static const char *_axes[] = { "Left Stick X", "Left Stick Y", "Right Stick X", "Right Stick Y", "", "", "L2", "R2" }; int InputDefault::get_joy_axis_index_from_string(String p_axis) { for (int i = 0; i < JOY_AXIS_MAX; i++) { if (p_axis == _axes[i]) { return i; } } ERR_FAIL_V(-1); } 

Advertencia de PVS-Studio: V557 CWE-125 Array overrun es posible. El valor del índice 'i' podría alcanzar 9. input_default.cpp 1119

La matriz _axes consta de ocho elementos. En este caso, la constante JOY_AXIS_MAX , que establece el número de iteraciones del bucle, es 10. Resulta que el bucle va más allá del límite de la matriz.

Error N19

Y la última función muy extraña, que, aparentemente, está diseñada para probar algo. La función es larga, así que la mostraré como una imagen (haga clic en la imagen para ampliarla).

Función muy extraña



Advertencia de PVS-Studio: V779 CWE-561 Código inalcanzable detectado. Es posible que haya un error presente. test_math.cpp 457

Hay varias declaraciones de retorno incondicionales en una función. En la imagen los marqué con óvalos rojos. Parece que se recopilaron varias pruebas unitarias diferentes para esta función, pero se olvidó de eliminar los NULL de retorno adicionales. Como resultado, la función no verifica lo que debería verificar. Casi todo el cuerpo de una función consiste en código inalcanzable.

Quizás, por supuesto, esta es una especie de idea astuta. Pero me parece que esto sucedió por casualidad y que el código debería repararse.

Terminemos con esto. Seguramente, si observa detenidamente el informe del analizador, puede encontrar otros errores. Sin embargo, incluso escribió más que suficiente para escribir un artículo. Entonces será aburrido para mí y para el lector :).

Conclusión


El artículo describe errores que no existirían si el código se analizara regularmente utilizando PVS-Studio. Sin embargo, lo que es más importante, utilizando análisis regulares, uno podría encontrar y eliminar muchos otros errores de inmediato. Mi colega describió esta idea con más detalle en su nota: “ Filosofía del análisis de código estático: tenemos 100 programadores, el analizador encontró pocos errores, ¿es inútil? ". Recomiendo pasar 10 minutos leyendo este breve pero muy importante artículo.

Gracias por su atencion Invito a todos a descargar y probar el análisis estático de PVS-Studio para probar sus propios proyectos.



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Andrey Karpov. Godot: sobre el uso regular de analizadores estáticos .

Source: https://habr.com/ru/post/es431200/


All Articles