blob: 2fd6714e84787400935789c5419f429c0080b47a [file] [log] [blame]
Pablo Galindo37494b42021-04-14 02:36:07 +01001#include "Python.h"
Pablo Galindo5bf8bf22021-04-14 15:10:33 +01002#include "frameobject.h"
Pablo Galindo37494b42021-04-14 02:36:07 +01003
4#include "pycore_pyerrors.h"
5
6#define MAX_DISTANCE 3
Pablo Galindo3ab4bea2021-04-17 22:26:54 +01007#define MAX_CANDIDATE_ITEMS 160
Pablo Galindo5bf8bf22021-04-14 15:10:33 +01008#define MAX_STRING_SIZE 25
Pablo Galindo37494b42021-04-14 02:36:07 +01009
10/* Calculate the Levenshtein distance between string1 and string2 */
Pablo Galindo0b1c1692021-04-17 23:28:45 +010011static Py_ssize_t
Pablo Galindo3fc65b92021-04-15 00:03:43 +010012levenshtein_distance(const char *a, size_t a_size,
13 const char *b, size_t b_size) {
Pablo Galindo37494b42021-04-14 02:36:07 +010014
15 if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) {
16 return 0;
17 }
18
19 // Both strings are the same (by identity)
20 if (a == b) {
21 return 0;
22 }
23
24 // The first string is empty
25 if (a_size == 0) {
26 return b_size;
27 }
28
29 // The second string is empty
30 if (b_size == 0) {
31 return a_size;
32 }
33
34 size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t));
35 if (buffer == NULL) {
Pablo Galindo0b1c1692021-04-17 23:28:45 +010036 return -1;
Pablo Galindo37494b42021-04-14 02:36:07 +010037 }
38
39 // Initialize the buffer row
40 size_t index = 0;
41 while (index < a_size) {
42 buffer[index] = index + 1;
43 index++;
44 }
45
46 size_t b_index = 0;
47 size_t result = 0;
48 while (b_index < b_size) {
49 char code = b[b_index];
50 size_t distance = result = b_index++;
51 index = SIZE_MAX;
52 while (++index < a_size) {
53 size_t b_distance = code == a[index] ? distance : distance + 1;
54 distance = buffer[index];
55 if (distance > result) {
56 if (b_distance > result) {
57 result = result + 1;
58 } else {
59 result = b_distance;
60 }
61 } else {
62 if (b_distance > distance) {
63 result = distance + 1;
64 } else {
65 result = b_distance;
66 }
67 }
68 buffer[index] = result;
69 }
70 }
71 PyMem_Free(buffer);
72 return result;
73}
74
75static inline PyObject *
76calculate_suggestions(PyObject *dir,
77 PyObject *name) {
78 assert(!PyErr_Occurred());
79 assert(PyList_CheckExact(dir));
80
81 Py_ssize_t dir_size = PyList_GET_SIZE(dir);
82 if (dir_size >= MAX_CANDIDATE_ITEMS) {
83 return NULL;
84 }
85
86 Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
87 PyObject *suggestion = NULL;
Pablo Galindo3fc65b92021-04-15 00:03:43 +010088 Py_ssize_t name_size;
89 const char *name_str = PyUnicode_AsUTF8AndSize(name, &name_size);
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010090 if (name_str == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010091 return NULL;
92 }
Pablo Galindo37494b42021-04-14 02:36:07 +010093 for (int i = 0; i < dir_size; ++i) {
94 PyObject *item = PyList_GET_ITEM(dir, i);
Pablo Galindo3fc65b92021-04-15 00:03:43 +010095 Py_ssize_t item_size;
96 const char *item_str = PyUnicode_AsUTF8AndSize(item, &item_size);
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010097 if (item_str == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010098 return NULL;
Pablo Galindo37494b42021-04-14 02:36:07 +010099 }
Pablo Galindo3fc65b92021-04-15 00:03:43 +0100100 Py_ssize_t current_distance = levenshtein_distance(
101 name_str, name_size, item_str, item_size);
Pablo Galindo0b1c1692021-04-17 23:28:45 +0100102 if (current_distance == -1) {
103 return NULL;
104 }
Dennis Sweeney284c52d2021-04-26 20:22:27 -0400105 if (current_distance == 0 ||
106 current_distance > MAX_DISTANCE ||
107 current_distance * 2 > name_size)
108 {
Pablo Galindo37494b42021-04-14 02:36:07 +0100109 continue;
110 }
111 if (!suggestion || current_distance < suggestion_distance) {
112 suggestion = item;
113 suggestion_distance = current_distance;
114 }
115 }
116 if (!suggestion) {
117 return NULL;
118 }
119 Py_INCREF(suggestion);
120 return suggestion;
121}
122
123static PyObject *
124offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
125 PyObject *name = exc->name; // borrowed reference
126 PyObject *obj = exc->obj; // borrowed reference
127
128 // Abort if we don't have an attribute name or we have an invalid one
129 if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) {
130 return NULL;
131 }
132
133 PyObject *dir = PyObject_Dir(obj);
134 if (dir == NULL) {
135 return NULL;
136 }
137
138 PyObject *suggestions = calculate_suggestions(dir, name);
139 Py_DECREF(dir);
140 return suggestions;
141}
142
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100143
144static PyObject *
145offer_suggestions_for_name_error(PyNameErrorObject *exc) {
146 PyObject *name = exc->name; // borrowed reference
147 PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference
Pablo Galindo3fc65b92021-04-15 00:03:43 +0100148 // Abort if we don't have a variable name or we have an invalid one
149 // or if we don't have a traceback to work with
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100150 if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) {
151 return NULL;
152 }
153
154 // Move to the traceback of the exception
155 while (traceback->tb_next != NULL) {
156 traceback = traceback->tb_next;
157 }
158
159 PyFrameObject *frame = traceback->tb_frame;
160 assert(frame != NULL);
161 PyCodeObject *code = frame->f_code;
162 assert(code != NULL && code->co_varnames != NULL);
163 PyObject *dir = PySequence_List(code->co_varnames);
164 if (dir == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100165 return NULL;
166 }
167
168 PyObject *suggestions = calculate_suggestions(dir, name);
169 Py_DECREF(dir);
170 if (suggestions != NULL) {
171 return suggestions;
172 }
173
174 dir = PySequence_List(frame->f_globals);
175 if (dir == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100176 return NULL;
177 }
178 suggestions = calculate_suggestions(dir, name);
179 Py_DECREF(dir);
Pablo Galindo3ab4bea2021-04-17 22:26:54 +0100180 if (suggestions != NULL) {
181 return suggestions;
182 }
183
184 dir = PySequence_List(frame->f_builtins);
185 if (dir == NULL) {
186 return NULL;
187 }
188 suggestions = calculate_suggestions(dir, name);
189 Py_DECREF(dir);
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100190
191 return suggestions;
192}
193
Pablo Galindo37494b42021-04-14 02:36:07 +0100194// Offer suggestions for a given exception. Returns a python string object containing the
Pablo Galindoe07f4ab2021-04-14 18:58:28 +0100195// suggestions. This function returns NULL if no suggestion was found or if an exception happened,
196// users must call PyErr_Occurred() to disambiguate.
Pablo Galindo37494b42021-04-14 02:36:07 +0100197PyObject *_Py_Offer_Suggestions(PyObject *exception) {
198 PyObject *result = NULL;
Pablo Galindoe07f4ab2021-04-14 18:58:28 +0100199 assert(!PyErr_Occurred());
Pablo Galindo0ad81d42021-04-16 17:12:03 +0100200 if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_AttributeError)) {
Pablo Galindo37494b42021-04-14 02:36:07 +0100201 result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
Pablo Galindo0ad81d42021-04-16 17:12:03 +0100202 } else if (Py_IS_TYPE(exception, (PyTypeObject*)PyExc_NameError)) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100203 result = offer_suggestions_for_name_error((PyNameErrorObject *) exception);
Pablo Galindo37494b42021-04-14 02:36:07 +0100204 }
Pablo Galindo37494b42021-04-14 02:36:07 +0100205 return result;
206}
207