You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

207 lines
5.8 KiB

  1. #include "Python.h"
  2. #include "frameobject.h"
  3. #include "pycore_pyerrors.h"
  4. #define MAX_DISTANCE 3
  5. #define MAX_CANDIDATE_ITEMS 160
  6. #define MAX_STRING_SIZE 25
  7. /* Calculate the Levenshtein distance between string1 and string2 */
  8. static Py_ssize_t
  9. levenshtein_distance(const char *a, size_t a_size,
  10. const char *b, size_t b_size) {
  11. if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) {
  12. return 0;
  13. }
  14. // Both strings are the same (by identity)
  15. if (a == b) {
  16. return 0;
  17. }
  18. // The first string is empty
  19. if (a_size == 0) {
  20. return b_size;
  21. }
  22. // The second string is empty
  23. if (b_size == 0) {
  24. return a_size;
  25. }
  26. size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t));
  27. if (buffer == NULL) {
  28. return -1;
  29. }
  30. // Initialize the buffer row
  31. size_t index = 0;
  32. while (index < a_size) {
  33. buffer[index] = index + 1;
  34. index++;
  35. }
  36. size_t b_index = 0;
  37. size_t result = 0;
  38. while (b_index < b_size) {
  39. char code = b[b_index];
  40. size_t distance = result = b_index++;
  41. index = SIZE_MAX;
  42. while (++index < a_size) {
  43. size_t b_distance = code == a[index] ? distance : distance + 1;
  44. distance = buffer[index];
  45. if (distance > result) {
  46. if (b_distance > result) {
  47. result = result + 1;
  48. } else {
  49. result = b_distance;
  50. }
  51. } else {
  52. if (b_distance > distance) {
  53. result = distance + 1;
  54. } else {
  55. result = b_distance;
  56. }
  57. }
  58. buffer[index] = result;
  59. }
  60. }
  61. PyMem_Free(buffer);
  62. return result;
  63. }
  64. static inline PyObject *
  65. calculate_suggestions(PyObject *dir,
  66. PyObject *name) {
  67. assert(!PyErr_Occurred());
  68. assert(PyList_CheckExact(dir));
  69. Py_ssize_t dir_size = PyList_GET_SIZE(dir);
  70. if (dir_size >= MAX_CANDIDATE_ITEMS) {
  71. return NULL;
  72. }
  73. Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
  74. PyObject *suggestion = NULL;
  75. Py_ssize_t name_size;
  76. const char *name_str = PyUnicode_AsUTF8AndSize(name, &name_size);
  77. if (name_str == NULL) {
  78. return NULL;
  79. }
  80. for (int i = 0; i < dir_size; ++i) {
  81. PyObject *item = PyList_GET_ITEM(dir, i);
  82. Py_ssize_t item_size;
  83. const char *item_str = PyUnicode_AsUTF8AndSize(item, &item_size);
  84. if (item_str == NULL) {
  85. return NULL;
  86. }
  87. Py_ssize_t current_distance = levenshtein_distance(
  88. name_str, name_size, item_str, item_size);
  89. if (current_distance == -1) {
  90. return NULL;
  91. }
  92. if (current_distance == 0 ||
  93. current_distance > MAX_DISTANCE ||
  94. current_distance * 2 > name_size)
  95. {
  96. continue;
  97. }
  98. if (!suggestion || current_distance < suggestion_distance) {
  99. suggestion = item;
  100. suggestion_distance = current_distance;
  101. }
  102. }
  103. if (!suggestion) {
  104. return NULL;
  105. }
  106. Py_INCREF(suggestion);
  107. return suggestion;
  108. }
  109. static PyObject *
  110. offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
  111. PyObject *name = exc->name; // borrowed reference
  112. PyObject *obj = exc->obj; // borrowed reference
  113. // Abort if we don't have an attribute name or we have an invalid one
  114. if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) {
  115. return NULL;
  116. }
  117. PyObject *dir = PyObject_Dir(obj);
  118. if (dir == NULL) {
  119. return NULL;
  120. }
  121. PyObject *suggestions = calculate_suggestions(dir, name);
  122. Py_DECREF(dir);
  123. return suggestions;
  124. }
  125. static PyObject *
  126. offer_suggestions_for_name_error(PyNameErrorObject *exc) {
  127. PyObject *name = exc->name; // borrowed reference
  128. PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference
  129. // Abort if we don't have a variable name or we have an invalid one
  130. // or if we don't have a traceback to work with
  131. if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) {
  132. return NULL;
  133. }
  134. // Move to the traceback of the exception
  135. while (traceback->tb_next != NULL) {
  136. traceback = traceback->tb_next;
  137. }
  138. PyFrameObject *frame = traceback->tb_frame;
  139. assert(frame != NULL);
  140. PyCodeObject *code = frame->f_code;
  141. assert(code != NULL && code->co_varnames != NULL);
  142. PyObject *dir = PySequence_List(code->co_varnames);
  143. if (dir == NULL) {
  144. return NULL;
  145. }
  146. PyObject *suggestions = calculate_suggestions(dir, name);
  147. Py_DECREF(dir);
  148. if (suggestions != NULL) {
  149. return suggestions;
  150. }
  151. dir = PySequence_List(frame->f_globals);
  152. if (dir == NULL) {
  153. return NULL;
  154. }
  155. suggestions = calculate_suggestions(dir, name);
  156. Py_DECREF(dir);
  157. if (suggestions != NULL) {
  158. return suggestions;
  159. }
  160. dir = PySequence_List(frame->f_builtins);
  161. if (dir == NULL) {
  162. return NULL;
  163. }
  164. suggestions = calculate_suggestions(dir, name);
  165. Py_DECREF(dir);
  166. return suggestions;
  167. }
  168. // Offer suggestions for a given exception. Returns a python string object containing the
  169. // suggestions. This function returns NULL if no suggestion was found or if an exception happened,
  170. // users must call PyErr_Occurred() to disambiguate.
  171. PyObject *_Py_Offer_Suggestions(PyObject *exception) {
  172. PyObject *result = NULL;
  173. assert(!PyErr_Occurred());
  174. if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_AttributeError)) {
  175. result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
  176. } else if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_NameError)) {
  177. result = offer_suggestions_for_name_error((PyNameErrorObject *) exception);
  178. }
  179. return result;
  180. }