blob: bdc8e2fd1bb0805da1ac798a4e227b5a84a4a2ec [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
7#define MAX_CANDIDATE_ITEMS 100
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 */
11static size_t
12levenshtein_distance(const char *a, const char *b) {
Pablo Galindo37494b42021-04-14 02:36:07 +010013
14 const size_t a_size = strlen(a);
15 const size_t b_size = strlen(b);
16
17 if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) {
18 return 0;
19 }
20
21 // Both strings are the same (by identity)
22 if (a == b) {
23 return 0;
24 }
25
26 // The first string is empty
27 if (a_size == 0) {
28 return b_size;
29 }
30
31 // The second string is empty
32 if (b_size == 0) {
33 return a_size;
34 }
35
36 size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t));
37 if (buffer == NULL) {
38 return 0;
39 }
40
41 // Initialize the buffer row
42 size_t index = 0;
43 while (index < a_size) {
44 buffer[index] = index + 1;
45 index++;
46 }
47
48 size_t b_index = 0;
49 size_t result = 0;
50 while (b_index < b_size) {
51 char code = b[b_index];
52 size_t distance = result = b_index++;
53 index = SIZE_MAX;
54 while (++index < a_size) {
55 size_t b_distance = code == a[index] ? distance : distance + 1;
56 distance = buffer[index];
57 if (distance > result) {
58 if (b_distance > result) {
59 result = result + 1;
60 } else {
61 result = b_distance;
62 }
63 } else {
64 if (b_distance > distance) {
65 result = distance + 1;
66 } else {
67 result = b_distance;
68 }
69 }
70 buffer[index] = result;
71 }
72 }
73 PyMem_Free(buffer);
74 return result;
75}
76
77static inline PyObject *
78calculate_suggestions(PyObject *dir,
79 PyObject *name) {
80 assert(!PyErr_Occurred());
81 assert(PyList_CheckExact(dir));
82
83 Py_ssize_t dir_size = PyList_GET_SIZE(dir);
84 if (dir_size >= MAX_CANDIDATE_ITEMS) {
85 return NULL;
86 }
87
88 Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
89 PyObject *suggestion = NULL;
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010090 const char *name_str = PyUnicode_AsUTF8(name);
91 if (name_str == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010092 return NULL;
93 }
Pablo Galindo37494b42021-04-14 02:36:07 +010094 for (int i = 0; i < dir_size; ++i) {
95 PyObject *item = PyList_GET_ITEM(dir, i);
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010096 const char *item_str = PyUnicode_AsUTF8(item);
97 if (item_str == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +010098 return NULL;
Pablo Galindo37494b42021-04-14 02:36:07 +010099 }
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100100 Py_ssize_t current_distance = levenshtein_distance(name_str, item_str);
Pablo Galindo37494b42021-04-14 02:36:07 +0100101 if (current_distance == 0 || current_distance > MAX_DISTANCE) {
102 continue;
103 }
104 if (!suggestion || current_distance < suggestion_distance) {
105 suggestion = item;
106 suggestion_distance = current_distance;
107 }
108 }
109 if (!suggestion) {
110 return NULL;
111 }
112 Py_INCREF(suggestion);
113 return suggestion;
114}
115
116static PyObject *
117offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
118 PyObject *name = exc->name; // borrowed reference
119 PyObject *obj = exc->obj; // borrowed reference
120
121 // Abort if we don't have an attribute name or we have an invalid one
122 if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) {
123 return NULL;
124 }
125
126 PyObject *dir = PyObject_Dir(obj);
127 if (dir == NULL) {
128 return NULL;
129 }
130
131 PyObject *suggestions = calculate_suggestions(dir, name);
132 Py_DECREF(dir);
133 return suggestions;
134}
135
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100136
137static PyObject *
138offer_suggestions_for_name_error(PyNameErrorObject *exc) {
139 PyObject *name = exc->name; // borrowed reference
140 PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference
141 // Abort if we don't have an attribute name or we have an invalid one
142 if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) {
143 return NULL;
144 }
145
146 // Move to the traceback of the exception
147 while (traceback->tb_next != NULL) {
148 traceback = traceback->tb_next;
149 }
150
151 PyFrameObject *frame = traceback->tb_frame;
152 assert(frame != NULL);
153 PyCodeObject *code = frame->f_code;
154 assert(code != NULL && code->co_varnames != NULL);
155 PyObject *dir = PySequence_List(code->co_varnames);
156 if (dir == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100157 return NULL;
158 }
159
160 PyObject *suggestions = calculate_suggestions(dir, name);
161 Py_DECREF(dir);
162 if (suggestions != NULL) {
163 return suggestions;
164 }
165
166 dir = PySequence_List(frame->f_globals);
167 if (dir == NULL) {
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100168 return NULL;
169 }
170 suggestions = calculate_suggestions(dir, name);
171 Py_DECREF(dir);
172
173 return suggestions;
174}
175
Pablo Galindo37494b42021-04-14 02:36:07 +0100176// Offer suggestions for a given exception. Returns a python string object containing the
Pablo Galindoe07f4ab2021-04-14 18:58:28 +0100177// suggestions. This function returns NULL if no suggestion was found or if an exception happened,
178// users must call PyErr_Occurred() to disambiguate.
Pablo Galindo37494b42021-04-14 02:36:07 +0100179PyObject *_Py_Offer_Suggestions(PyObject *exception) {
180 PyObject *result = NULL;
Pablo Galindoe07f4ab2021-04-14 18:58:28 +0100181 assert(!PyErr_Occurred());
Pablo Galindo37494b42021-04-14 02:36:07 +0100182 if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) {
183 result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
Pablo Galindo5bf8bf22021-04-14 15:10:33 +0100184 } else if (PyErr_GivenExceptionMatches(exception, PyExc_NameError)) {
185 result = offer_suggestions_for_name_error((PyNameErrorObject *) exception);
Pablo Galindo37494b42021-04-14 02:36:07 +0100186 }
Pablo Galindo37494b42021-04-14 02:36:07 +0100187 return result;
188}
189