| #include "Python.h" |
| #include "frameobject.h" |
| |
| #include "pycore_pyerrors.h" |
| |
| #define MAX_DISTANCE 3 |
| #define MAX_CANDIDATE_ITEMS 160 |
| #define MAX_STRING_SIZE 25 |
| |
| /* Calculate the Levenshtein distance between string1 and string2 */ |
| static Py_ssize_t |
| levenshtein_distance(const char *a, size_t a_size, |
| const char *b, size_t b_size) { |
| |
| if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) { |
| return 0; |
| } |
| |
| // Both strings are the same (by identity) |
| if (a == b) { |
| return 0; |
| } |
| |
| // The first string is empty |
| if (a_size == 0) { |
| return b_size; |
| } |
| |
| // The second string is empty |
| if (b_size == 0) { |
| return a_size; |
| } |
| |
| size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t)); |
| if (buffer == NULL) { |
| return -1; |
| } |
| |
| // Initialize the buffer row |
| size_t index = 0; |
| while (index < a_size) { |
| buffer[index] = index + 1; |
| index++; |
| } |
| |
| size_t b_index = 0; |
| size_t result = 0; |
| while (b_index < b_size) { |
| char code = b[b_index]; |
| size_t distance = result = b_index++; |
| index = SIZE_MAX; |
| while (++index < a_size) { |
| size_t b_distance = code == a[index] ? distance : distance + 1; |
| distance = buffer[index]; |
| if (distance > result) { |
| if (b_distance > result) { |
| result = result + 1; |
| } else { |
| result = b_distance; |
| } |
| } else { |
| if (b_distance > distance) { |
| result = distance + 1; |
| } else { |
| result = b_distance; |
| } |
| } |
| buffer[index] = result; |
| } |
| } |
| PyMem_Free(buffer); |
| return result; |
| } |
| |
| static inline PyObject * |
| calculate_suggestions(PyObject *dir, |
| PyObject *name) { |
| assert(!PyErr_Occurred()); |
| assert(PyList_CheckExact(dir)); |
| |
| Py_ssize_t dir_size = PyList_GET_SIZE(dir); |
| if (dir_size >= MAX_CANDIDATE_ITEMS) { |
| return NULL; |
| } |
| |
| Py_ssize_t suggestion_distance = PyUnicode_GetLength(name); |
| PyObject *suggestion = NULL; |
| Py_ssize_t name_size; |
| const char *name_str = PyUnicode_AsUTF8AndSize(name, &name_size); |
| if (name_str == NULL) { |
| return NULL; |
| } |
| for (int i = 0; i < dir_size; ++i) { |
| PyObject *item = PyList_GET_ITEM(dir, i); |
| Py_ssize_t item_size; |
| const char *item_str = PyUnicode_AsUTF8AndSize(item, &item_size); |
| if (item_str == NULL) { |
| return NULL; |
| } |
| Py_ssize_t current_distance = levenshtein_distance( |
| name_str, name_size, item_str, item_size); |
| if (current_distance == -1) { |
| return NULL; |
| } |
| if (current_distance == 0 || |
| current_distance > MAX_DISTANCE || |
| current_distance * 2 > name_size) |
| { |
| continue; |
| } |
| if (!suggestion || current_distance < suggestion_distance) { |
| suggestion = item; |
| suggestion_distance = current_distance; |
| } |
| } |
| if (!suggestion) { |
| return NULL; |
| } |
| Py_INCREF(suggestion); |
| return suggestion; |
| } |
| |
| static PyObject * |
| offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) { |
| PyObject *name = exc->name; // borrowed reference |
| PyObject *obj = exc->obj; // borrowed reference |
| |
| // Abort if we don't have an attribute name or we have an invalid one |
| if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) { |
| return NULL; |
| } |
| |
| PyObject *dir = PyObject_Dir(obj); |
| if (dir == NULL) { |
| return NULL; |
| } |
| |
| PyObject *suggestions = calculate_suggestions(dir, name); |
| Py_DECREF(dir); |
| return suggestions; |
| } |
| |
| |
| static PyObject * |
| offer_suggestions_for_name_error(PyNameErrorObject *exc) { |
| PyObject *name = exc->name; // borrowed reference |
| PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference |
| // Abort if we don't have a variable name or we have an invalid one |
| // or if we don't have a traceback to work with |
| if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) { |
| return NULL; |
| } |
| |
| // Move to the traceback of the exception |
| while (traceback->tb_next != NULL) { |
| traceback = traceback->tb_next; |
| } |
| |
| PyFrameObject *frame = traceback->tb_frame; |
| assert(frame != NULL); |
| PyCodeObject *code = frame->f_code; |
| assert(code != NULL && code->co_varnames != NULL); |
| PyObject *dir = PySequence_List(code->co_varnames); |
| if (dir == NULL) { |
| return NULL; |
| } |
| |
| PyObject *suggestions = calculate_suggestions(dir, name); |
| Py_DECREF(dir); |
| if (suggestions != NULL) { |
| return suggestions; |
| } |
| |
| dir = PySequence_List(frame->f_globals); |
| if (dir == NULL) { |
| return NULL; |
| } |
| suggestions = calculate_suggestions(dir, name); |
| Py_DECREF(dir); |
| if (suggestions != NULL) { |
| return suggestions; |
| } |
| |
| dir = PySequence_List(frame->f_builtins); |
| if (dir == NULL) { |
| return NULL; |
| } |
| suggestions = calculate_suggestions(dir, name); |
| Py_DECREF(dir); |
| |
| return suggestions; |
| } |
| |
| // Offer suggestions for a given exception. Returns a python string object containing the |
| // suggestions. This function returns NULL if no suggestion was found or if an exception happened, |
| // users must call PyErr_Occurred() to disambiguate. |
| PyObject *_Py_Offer_Suggestions(PyObject *exception) { |
| PyObject *result = NULL; |
| assert(!PyErr_Occurred()); |
| if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_AttributeError)) { |
| result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception); |
| } else if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_NameError)) { |
| result = offer_suggestions_for_name_error((PyNameErrorObject *) exception); |
| } |
| return result; |
| } |
| |