blob: 2c0858d558d00ca8e087e1829398bcf8bda78884 [file] [log] [blame]
Pablo Galindo37494b42021-04-14 02:36:07 +01001#include "Python.h"
2
3#include "pycore_pyerrors.h"
4
5#define MAX_DISTANCE 3
6#define MAX_CANDIDATE_ITEMS 100
7#define MAX_STRING_SIZE 20
8
9/* Calculate the Levenshtein distance between string1 and string2 */
10static size_t
11levenshtein_distance(const char *a, const char *b) {
12 if (a == NULL || b == NULL) {
13 return 0;
14 }
15
16 const size_t a_size = strlen(a);
17 const size_t b_size = strlen(b);
18
19 if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) {
20 return 0;
21 }
22
23 // Both strings are the same (by identity)
24 if (a == b) {
25 return 0;
26 }
27
28 // The first string is empty
29 if (a_size == 0) {
30 return b_size;
31 }
32
33 // The second string is empty
34 if (b_size == 0) {
35 return a_size;
36 }
37
38 size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t));
39 if (buffer == NULL) {
40 return 0;
41 }
42
43 // Initialize the buffer row
44 size_t index = 0;
45 while (index < a_size) {
46 buffer[index] = index + 1;
47 index++;
48 }
49
50 size_t b_index = 0;
51 size_t result = 0;
52 while (b_index < b_size) {
53 char code = b[b_index];
54 size_t distance = result = b_index++;
55 index = SIZE_MAX;
56 while (++index < a_size) {
57 size_t b_distance = code == a[index] ? distance : distance + 1;
58 distance = buffer[index];
59 if (distance > result) {
60 if (b_distance > result) {
61 result = result + 1;
62 } else {
63 result = b_distance;
64 }
65 } else {
66 if (b_distance > distance) {
67 result = distance + 1;
68 } else {
69 result = b_distance;
70 }
71 }
72 buffer[index] = result;
73 }
74 }
75 PyMem_Free(buffer);
76 return result;
77}
78
79static inline PyObject *
80calculate_suggestions(PyObject *dir,
81 PyObject *name) {
82 assert(!PyErr_Occurred());
83 assert(PyList_CheckExact(dir));
84
85 Py_ssize_t dir_size = PyList_GET_SIZE(dir);
86 if (dir_size >= MAX_CANDIDATE_ITEMS) {
87 return NULL;
88 }
89
90 Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
91 PyObject *suggestion = NULL;
92 for (int i = 0; i < dir_size; ++i) {
93 PyObject *item = PyList_GET_ITEM(dir, i);
94 const char *name_str = PyUnicode_AsUTF8(name);
95 if (name_str == NULL) {
96 PyErr_Clear();
97 continue;
98 }
99 Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(item));
100 if (current_distance == 0 || current_distance > MAX_DISTANCE) {
101 continue;
102 }
103 if (!suggestion || current_distance < suggestion_distance) {
104 suggestion = item;
105 suggestion_distance = current_distance;
106 }
107 }
108 if (!suggestion) {
109 return NULL;
110 }
111 Py_INCREF(suggestion);
112 return suggestion;
113}
114
115static PyObject *
116offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
117 PyObject *name = exc->name; // borrowed reference
118 PyObject *obj = exc->obj; // borrowed reference
119
120 // Abort if we don't have an attribute name or we have an invalid one
121 if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) {
122 return NULL;
123 }
124
125 PyObject *dir = PyObject_Dir(obj);
126 if (dir == NULL) {
127 return NULL;
128 }
129
130 PyObject *suggestions = calculate_suggestions(dir, name);
131 Py_DECREF(dir);
132 return suggestions;
133}
134
135// Offer suggestions for a given exception. Returns a python string object containing the
136// suggestions. This function does not raise exceptions and returns NULL if no suggestion was found.
137PyObject *_Py_Offer_Suggestions(PyObject *exception) {
138 PyObject *result = NULL;
139 assert(!PyErr_Occurred()); // Check that we are not going to clean any existing exception
140 if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) {
141 result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
142 }
143 assert(!PyErr_Occurred());
144 return result;
145}
146