We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Extend the whiteboard feature to consider all possible whiteboard words
https://github.com/mozilla/bugbug/blob/1784a10346d9dedcb2e18e076f3d482e39be93a1/bugbug/bug_features.py#L83

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bugbug/bug_features.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 # You can obtain one at http://mozilla.org/MPL/2.0/.
5 
6 import re
7 from datetime import datetime
8 from datetime import timezone
9 
10 import pandas as pd
11 from libmozdata import versions
12 from sklearn.base import BaseEstimator
13 from sklearn.base import TransformerMixin
14 
15 from bugbug import bug_snapshot
16 from bugbug import repository
17 
18 
19 def field(bug, field):
20     if field in bug and bug[field] != '---':
21         return bug[field]
22 
23     return None
24 
25 
26 class has_str(object):
27     def __call__(self, bug):
28         return field(bug, 'cf_has_str')
29 
30 
31 class has_regression_range(object):
32     def __call__(self, bug):
33         return field(bug, 'cf_has_regression_range')
34 
35 
36 class has_crash_signature(object):
37     def __call__(self, bug):
38         return 'cf_crash_signature' in bug and bug['cf_crash_signature'] != ''
39 
40 
41 class keywords(object):
42     def __init__(self, to_ignore=set()):
43         self.to_ignore = to_ignore
44 
45     def __call__(self, bug):
46         keywords = []
47         subkeywords = []
48         for keyword in bug['keywords']:
49             if keyword in self.to_ignore:
50                 continue
51 
52             keywords.append(keyword)
53 
54             if keyword.startswith('sec-'):
55                 subkeywords.append('sec-')
56             elif keyword.startswith('csectype-'):
57                 subkeywords.append('csectype-')
58         return keywords + subkeywords
59 
60 
61 class severity(object):
62     def __call__(self, bug):
63         return field(bug, 'severity')
64 
65 
66 class is_coverity_issue(object):
67     def __call__(self, bug):
68         return re.search('[CID ?[0-9]+]', bug['summary']) is not None or re.search('[CID ?[0-9]+]', bug['whiteboard']) is not None
69 
70 
71 class has_url(object):
72     def __call__(self, bug):
73         return bug['url'] != ''
74 
75 
76 class has_w3c_url(object):
77     def __call__(self, bug):
78         return 'w3c' in bug['url']
79 
80 
81 class has_github_url(object):
82     def __call__(self, bug):
83         return 'github' in bug['url']
84 
85 
86 class whiteboard(object):
87     def __call__(self, bug):
88         ret = []
89 
90         # TODO: Add any [XXX:YYY] that appears in the whiteboard as [XXX: only
91 
92         for elem in ['memshrink', '[ux]']:
93             if elem in bug['whiteboard'].lower():
94                 ret.append(elem)
95 
96         return ret
97 
98 
99 class patches(object):
100     def __call__(self, bug):
101         return sum(1 for a in bug['attachments'] if a['is_patch'] or a['content_type'] in ['text/x-review-board-request', 'text/x-phabricator-request'])
102 
103 
104 class landings(object):
105     def __call__(self, bug):
106         return sum(1 for c in bug['comments'] if '://hg.mozilla.org/' in c['text'])
107 
108 
109 class title(object):
110     def __call__(self, bug):
111         ret = []
112 
113         keywords = [
114             'fail',
115         ]
116         for keyword in keywords:
117             if keyword in bug['summary'].lower():
118                 ret.append(keyword)
119 
120         return ret
121 
122 
123 class product(object):
124     def __call__(self, bug):
125         return bug['product']
126 
127 
128 class component(object):
129     def __call__(self, bug):
130         return bug['component']
131 
132 
133 class is_mozillian(object):
134     def __call__(self, bug):
135         return any(bug['creator_detail']['email'].endswith(domain) for domain in ['@mozilla.com', '@mozilla.org'])
136 
137 
138 class delta_request_merge(object):
139     def __call__(self, bug):
140         for history in bug['history']:
141             for change in history['changes']:
142                 if change['added'].startswith('approval-mozilla'):
143                     uplift_request_datetime = datetime.strptime(history['when'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
144                     timedelta = versions.getCloserRelease(uplift_request_datetime)[1] - uplift_request_datetime
145                     return timedelta.days + timedelta.seconds / (24 * 60 * 60)
146 
147         return None
148 
149 
150 class commit_added(object):
151     def __call__(self, bug):
152         return sum(commit['added'] for commit in bug['commits'])
153 
154 
155 class commit_deleted(object):
156     def __call__(self, bug):
157         return sum(commit['deleted'] for commit in bug['commits'])
158 
159 
160 class commit_types(object):
161     def __call__(self, bug):
162         return sum((commit['types'] for commit in bug['commits']), [])
163 
164 
165 def cleanup_url(text):
166     text = re.sub(r'http[s]?://(hg.mozilla|searchfox|dxr.mozilla)\S+', '__CODE_REFERENCE_URL__', text)
167     return re.sub(r'http\S+', '__URL__', text)
168 
169 
170 def cleanup_fileref(text):
171     return re.sub(r'\w+\.py\b|\w+\.json\b|\w+\.js\b|\w+\.jsm\b|\w+\.html\b|\w+\.css\b|\w+\.c\b|\w+\.cpp\b|\w+\.h\b', '__FILE_REFERENCE__', text)
172 
173 
174 def cleanup_responses(text):
175     return re.sub('>[^\n]+', ' ', text)
176 
177 
178 def cleanup_hex(text):
179     return re.sub(r'\b0[xX][0-9a-fA-F]+\b', '__HEX_NUMBER__', text)
180 
181 
182 def cleanup_dll(text):
183     return re.sub(r'\w+(\.dll|\.so|\.dylib)\b', '__DLL_NAME__', text)
184 
185 
186 def cleanup_synonyms(text):
187     synonyms = [
188         ('safemode', ['safemode', 'safe mode']),
189         ('str', ['str', 'steps to reproduce', 'repro steps']),
190         ('uaf', ['uaf', 'use after free', 'use-after-free']),
191         ('asan', ['asan', 'address sanitizer', 'addresssanitizer']),
192         ('permafailure', ['permafailure', 'permafailing', 'permafail', 'perma failure', 'perma failing', 'perma fail', 'perma-failure', 'perma-failing', 'perma-fail']),
193         ('spec', ['spec', 'specification']),
194     ]
195 
196     for synonym_group, synonym_list in synonyms:
197         text = re.sub('|'.join(fr'\b{synonym}\b' for synonym in synonym_list), synonym_group, text, flags=re.IGNORECASE)
198 
199     return text
200 
201 
202 def cleanup_crash(text):
203     return re.sub(r'bp-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{6}[0-9]{6}\b', '__CRASH_STATS_LINK__', text)
204 
205 
206 class BugExtractor(BaseEstimator, TransformerMixin):
207     def __init__(self, feature_extractors, cleanup_functions, rollback=False, rollback_when=None, commit_data=False):
208         self.feature_extractors = feature_extractors
209         self.cleanup_functions = cleanup_functions
210         self.rollback = rollback
211         self.rollback_when = rollback_when
212         self.commit_map = repository.get_commit_map() if commit_data else None
213 
214     def fit(self, x, y=None):
215         return self
216 
217     def transform(self, bugs):
218         results = []
219 
220         for bug in bugs:
221             bug_id = bug['id']
222 
223             if self.rollback:
224                 bug = bug_snapshot.rollback(bug, self.rollback_when)
225 
226             data = {}
227 
228             if self.commit_map is not None:
229                 if bug_id in self.commit_map:
230                     bug['commits'] = self.commit_map[bug_id]
231                 else:
232                     bug['commits'] = []
233 
234             for f in self.feature_extractors:
235                 res = f(bug)
236 
237                 if res is None:
238                     continue
239 
240                 if isinstance(res, list):
241                     for item in res:
242                         data[f.__class__.__name__ + '-' + item] = 'True'
243                     continue
244 
245                 if isinstance(res, bool):
246                     res = str(res)
247 
248                 data[f.__class__.__name__] = res
249 
250             # TODO: Try simply using all possible fields instead of extracting features manually.
251 
252             for cleanup_function in self.cleanup_functions:
253                 bug['summary'] = cleanup_function(bug['summary'])
254                 for c in bug['comments']:
255                     c['text'] = cleanup_function(c['text'])
256 
257             result = {
258                 'data': data,
259                 'title': bug['summary'],
260                 'first_comment': bug['comments'][0]['text'],
261                 'comments': ' '.join([c['text'] for c in bug['comments']]),
262             }
263 
264             results.append(result)
265 
266         return pd.DataFrame(results)
267
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bugbug/bug_features.py b/bugbug/bug_features.py
--- a/bugbug/bug_features.py
+++ b/bugbug/bug_features.py
@@ -85,15 +85,26 @@
 
 class whiteboard(object):
     def __call__(self, bug):
-        ret = []
 
-        # TODO: Add any [XXX:YYY] that appears in the whiteboard as [XXX: only
+        # Split by '['
+        paren_splits = bug['whiteboard'].lower().split('[')
 
-        for elem in ['memshrink', '[ux]']:
-            if elem in bug['whiteboard'].lower():
-                ret.append(elem)
+        # Split splits by space if they weren't in [ and ].
+        splits = []
+        for paren_split in paren_splits:
+            if ']' in paren_split:
+                paren_split = paren_split.split(']')
+                splits += paren_split
+            else:
+                splits += paren_split.split(' ')
 
-        return ret
+        # Remove empty splits and strip
+        splits = [split.strip() for split in splits if split.strip() != '']
+
+        # For splits which contain ':', return both the whole string and the string before ':'.
+        splits += [split.split(':', 1)[0] for split in splits if ':' in split]
+
+        return splits
 
 
 class patches(object):

{"golden_diff": "diff --git a/bugbug/bug_features.py b/bugbug/bug_features.py\n--- a/bugbug/bug_features.py\n+++ b/bugbug/bug_features.py\n@@ -85,15 +85,26 @@\n \n class whiteboard(object):\n     def __call__(self, bug):\n-        ret = []\n \n-        # TODO: Add any [XXX:YYY] that appears in the whiteboard as [XXX: only\n+        # Split by '['\n+        paren_splits = bug['whiteboard'].lower().split('[')\n \n-        for elem in ['memshrink', '[ux]']:\n-            if elem in bug['whiteboard'].lower():\n-                ret.append(elem)\n+        # Split splits by space if they weren't in [ and ].\n+        splits = []\n+        for paren_split in paren_splits:\n+            if ']' in paren_split:\n+                paren_split = paren_split.split(']')\n+                splits += paren_split\n+            else:\n+                splits += paren_split.split(' ')\n \n-        return ret\n+        # Remove empty splits and strip\n+        splits = [split.strip() for split in splits if split.strip() != '']\n+\n+        # For splits which contain ':', return both the whole string and the string before ':'.\n+        splits += [split.split(':', 1)[0] for split in splits if ':' in split]\n+\n+        return splits\n \n \n class patches(object):\n", "issue": "Extend the whiteboard feature to consider all possible whiteboard words\nhttps://github.com/mozilla/bugbug/blob/1784a10346d9dedcb2e18e076f3d482e39be93a1/bugbug/bug_features.py#L83\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this file,\n# You can obtain one at http://mozilla.org/MPL/2.0/.\n\nimport re\nfrom datetime import datetime\nfrom datetime import timezone\n\nimport pandas as pd\nfrom libmozdata import versions\nfrom sklearn.base import BaseEstimator\nfrom sklearn.base import TransformerMixin\n\nfrom bugbug import bug_snapshot\nfrom bugbug import repository\n\n\ndef field(bug, field):\n    if field in bug and bug[field] != '---':\n        return bug[field]\n\n    return None\n\n\nclass has_str(object):\n    def __call__(self, bug):\n        return field(bug, 'cf_has_str')\n\n\nclass has_regression_range(object):\n    def __call__(self, bug):\n        return field(bug, 'cf_has_regression_range')\n\n\nclass has_crash_signature(object):\n    def __call__(self, bug):\n        return 'cf_crash_signature' in bug and bug['cf_crash_signature'] != ''\n\n\nclass keywords(object):\n    def __init__(self, to_ignore=set()):\n        self.to_ignore = to_ignore\n\n    def __call__(self, bug):\n        keywords = []\n        subkeywords = []\n        for keyword in bug['keywords']:\n            if keyword in self.to_ignore:\n                continue\n\n            keywords.append(keyword)\n\n            if keyword.startswith('sec-'):\n                subkeywords.append('sec-')\n            elif keyword.startswith('csectype-'):\n                subkeywords.append('csectype-')\n        return keywords + subkeywords\n\n\nclass severity(object):\n    def __call__(self, bug):\n        return field(bug, 'severity')\n\n\nclass is_coverity_issue(object):\n    def __call__(self, bug):\n        return re.search('[CID ?[0-9]+]', bug['summary']) is not None or re.search('[CID ?[0-9]+]', bug['whiteboard']) is not None\n\n\nclass has_url(object):\n    def __call__(self, bug):\n        return bug['url'] != ''\n\n\nclass has_w3c_url(object):\n    def __call__(self, bug):\n        return 'w3c' in bug['url']\n\n\nclass has_github_url(object):\n    def __call__(self, bug):\n        return 'github' in bug['url']\n\n\nclass whiteboard(object):\n    def __call__(self, bug):\n        ret = []\n\n        # TODO: Add any [XXX:YYY] that appears in the whiteboard as [XXX: only\n\n        for elem in ['memshrink', '[ux]']:\n            if elem in bug['whiteboard'].lower():\n                ret.append(elem)\n\n        return ret\n\n\nclass patches(object):\n    def __call__(self, bug):\n        return sum(1 for a in bug['attachments'] if a['is_patch'] or a['content_type'] in ['text/x-review-board-request', 'text/x-phabricator-request'])\n\n\nclass landings(object):\n    def __call__(self, bug):\n        return sum(1 for c in bug['comments'] if '://hg.mozilla.org/' in c['text'])\n\n\nclass title(object):\n    def __call__(self, bug):\n        ret = []\n\n        keywords = [\n            'fail',\n        ]\n        for keyword in keywords:\n            if keyword in bug['summary'].lower():\n                ret.append(keyword)\n\n        return ret\n\n\nclass product(object):\n    def __call__(self, bug):\n        return bug['product']\n\n\nclass component(object):\n    def __call__(self, bug):\n        return bug['component']\n\n\nclass is_mozillian(object):\n    def __call__(self, bug):\n        return any(bug['creator_detail']['email'].endswith(domain) for domain in ['@mozilla.com', '@mozilla.org'])\n\n\nclass delta_request_merge(object):\n    def __call__(self, bug):\n        for history in bug['history']:\n            for change in history['changes']:\n                if change['added'].startswith('approval-mozilla'):\n                    uplift_request_datetime = datetime.strptime(history['when'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)\n                    timedelta = versions.getCloserRelease(uplift_request_datetime)[1] - uplift_request_datetime\n                    return timedelta.days + timedelta.seconds / (24 * 60 * 60)\n\n        return None\n\n\nclass commit_added(object):\n    def __call__(self, bug):\n        return sum(commit['added'] for commit in bug['commits'])\n\n\nclass commit_deleted(object):\n    def __call__(self, bug):\n        return sum(commit['deleted'] for commit in bug['commits'])\n\n\nclass commit_types(object):\n    def __call__(self, bug):\n        return sum((commit['types'] for commit in bug['commits']), [])\n\n\ndef cleanup_url(text):\n    text = re.sub(r'http[s]?://(hg.mozilla|searchfox|dxr.mozilla)\\S+', '__CODE_REFERENCE_URL__', text)\n    return re.sub(r'http\\S+', '__URL__', text)\n\n\ndef cleanup_fileref(text):\n    return re.sub(r'\\w+\\.py\\b|\\w+\\.json\\b|\\w+\\.js\\b|\\w+\\.jsm\\b|\\w+\\.html\\b|\\w+\\.css\\b|\\w+\\.c\\b|\\w+\\.cpp\\b|\\w+\\.h\\b', '__FILE_REFERENCE__', text)\n\n\ndef cleanup_responses(text):\n    return re.sub('>[^\\n]+', ' ', text)\n\n\ndef cleanup_hex(text):\n    return re.sub(r'\\b0[xX][0-9a-fA-F]+\\b', '__HEX_NUMBER__', text)\n\n\ndef cleanup_dll(text):\n    return re.sub(r'\\w+(\\.dll|\\.so|\\.dylib)\\b', '__DLL_NAME__', text)\n\n\ndef cleanup_synonyms(text):\n    synonyms = [\n        ('safemode', ['safemode', 'safe mode']),\n        ('str', ['str', 'steps to reproduce', 'repro steps']),\n        ('uaf', ['uaf', 'use after free', 'use-after-free']),\n        ('asan', ['asan', 'address sanitizer', 'addresssanitizer']),\n        ('permafailure', ['permafailure', 'permafailing', 'permafail', 'perma failure', 'perma failing', 'perma fail', 'perma-failure', 'perma-failing', 'perma-fail']),\n        ('spec', ['spec', 'specification']),\n    ]\n\n    for synonym_group, synonym_list in synonyms:\n        text = re.sub('|'.join(fr'\\b{synonym}\\b' for synonym in synonym_list), synonym_group, text, flags=re.IGNORECASE)\n\n    return text\n\n\ndef cleanup_crash(text):\n    return re.sub(r'bp-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{6}[0-9]{6}\\b', '__CRASH_STATS_LINK__', text)\n\n\nclass BugExtractor(BaseEstimator, TransformerMixin):\n    def __init__(self, feature_extractors, cleanup_functions, rollback=False, rollback_when=None, commit_data=False):\n        self.feature_extractors = feature_extractors\n        self.cleanup_functions = cleanup_functions\n        self.rollback = rollback\n        self.rollback_when = rollback_when\n        self.commit_map = repository.get_commit_map() if commit_data else None\n\n    def fit(self, x, y=None):\n        return self\n\n    def transform(self, bugs):\n        results = []\n\n        for bug in bugs:\n            bug_id = bug['id']\n\n            if self.rollback:\n                bug = bug_snapshot.rollback(bug, self.rollback_when)\n\n            data = {}\n\n            if self.commit_map is not None:\n                if bug_id in self.commit_map:\n                    bug['commits'] = self.commit_map[bug_id]\n                else:\n                    bug['commits'] = []\n\n            for f in self.feature_extractors:\n                res = f(bug)\n\n                if res is None:\n                    continue\n\n                if isinstance(res, list):\n                    for item in res:\n                        data[f.__class__.__name__ + '-' + item] = 'True'\n                    continue\n\n                if isinstance(res, bool):\n                    res = str(res)\n\n                data[f.__class__.__name__] = res\n\n            # TODO: Try simply using all possible fields instead of extracting features manually.\n\n            for cleanup_function in self.cleanup_functions:\n                bug['summary'] = cleanup_function(bug['summary'])\n                for c in bug['comments']:\n                    c['text'] = cleanup_function(c['text'])\n\n            result = {\n                'data': data,\n                'title': bug['summary'],\n                'first_comment': bug['comments'][0]['text'],\n                'comments': ' '.join([c['text'] for c in bug['comments']]),\n            }\n\n            results.append(result)\n\n        return pd.DataFrame(results)\n", "path": "bugbug/bug_features.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this file,\n# You can obtain one at http://mozilla.org/MPL/2.0/.\n\nimport re\nfrom datetime import datetime\nfrom datetime import timezone\n\nimport pandas as pd\nfrom libmozdata import versions\nfrom sklearn.base import BaseEstimator\nfrom sklearn.base import TransformerMixin\n\nfrom bugbug import bug_snapshot\nfrom bugbug import repository\n\n\ndef field(bug, field):\n    if field in bug and bug[field] != '---':\n        return bug[field]\n\n    return None\n\n\nclass has_str(object):\n    def __call__(self, bug):\n        return field(bug, 'cf_has_str')\n\n\nclass has_regression_range(object):\n    def __call__(self, bug):\n        return field(bug, 'cf_has_regression_range')\n\n\nclass has_crash_signature(object):\n    def __call__(self, bug):\n        return 'cf_crash_signature' in bug and bug['cf_crash_signature'] != ''\n\n\nclass keywords(object):\n    def __init__(self, to_ignore=set()):\n        self.to_ignore = to_ignore\n\n    def __call__(self, bug):\n        keywords = []\n        subkeywords = []\n        for keyword in bug['keywords']:\n            if keyword in self.to_ignore:\n                continue\n\n            keywords.append(keyword)\n\n            if keyword.startswith('sec-'):\n                subkeywords.append('sec-')\n            elif keyword.startswith('csectype-'):\n                subkeywords.append('csectype-')\n        return keywords + subkeywords\n\n\nclass severity(object):\n    def __call__(self, bug):\n        return field(bug, 'severity')\n\n\nclass is_coverity_issue(object):\n    def __call__(self, bug):\n        return re.search('[CID ?[0-9]+]', bug['summary']) is not None or re.search('[CID ?[0-9]+]', bug['whiteboard']) is not None\n\n\nclass has_url(object):\n    def __call__(self, bug):\n        return bug['url'] != ''\n\n\nclass has_w3c_url(object):\n    def __call__(self, bug):\n        return 'w3c' in bug['url']\n\n\nclass has_github_url(object):\n    def __call__(self, bug):\n        return 'github' in bug['url']\n\n\nclass whiteboard(object):\n    def __call__(self, bug):\n\n        # Split by '['\n        paren_splits = bug['whiteboard'].lower().split('[')\n\n        # Split splits by space if they weren't in [ and ].\n        splits = []\n        for paren_split in paren_splits:\n            if ']' in paren_split:\n                paren_split = paren_split.split(']')\n                splits += paren_split\n            else:\n                splits += paren_split.split(' ')\n\n        # Remove empty splits and strip\n        splits = [split.strip() for split in splits if split.strip() != '']\n\n        # For splits which contain ':', return both the whole string and the string before ':'.\n        splits += [split.split(':', 1)[0] for split in splits if ':' in split]\n\n        return splits\n\n\nclass patches(object):\n    def __call__(self, bug):\n        return sum(1 for a in bug['attachments'] if a['is_patch'] or a['content_type'] in ['text/x-review-board-request', 'text/x-phabricator-request'])\n\n\nclass landings(object):\n    def __call__(self, bug):\n        return sum(1 for c in bug['comments'] if '://hg.mozilla.org/' in c['text'])\n\n\nclass title(object):\n    def __call__(self, bug):\n        ret = []\n\n        keywords = [\n            'fail',\n        ]\n        for keyword in keywords:\n            if keyword in bug['summary'].lower():\n                ret.append(keyword)\n\n        return ret\n\n\nclass product(object):\n    def __call__(self, bug):\n        return bug['product']\n\n\nclass component(object):\n    def __call__(self, bug):\n        return bug['component']\n\n\nclass is_mozillian(object):\n    def __call__(self, bug):\n        return any(bug['creator_detail']['email'].endswith(domain) for domain in ['@mozilla.com', '@mozilla.org'])\n\n\nclass delta_request_merge(object):\n    def __call__(self, bug):\n        for history in bug['history']:\n            for change in history['changes']:\n                if change['added'].startswith('approval-mozilla'):\n                    uplift_request_datetime = datetime.strptime(history['when'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)\n                    timedelta = versions.getCloserRelease(uplift_request_datetime)[1] - uplift_request_datetime\n                    return timedelta.days + timedelta.seconds / (24 * 60 * 60)\n\n        return None\n\n\nclass commit_added(object):\n    def __call__(self, bug):\n        return sum(commit['added'] for commit in bug['commits'])\n\n\nclass commit_deleted(object):\n    def __call__(self, bug):\n        return sum(commit['deleted'] for commit in bug['commits'])\n\n\nclass commit_types(object):\n    def __call__(self, bug):\n        return sum((commit['types'] for commit in bug['commits']), [])\n\n\ndef cleanup_url(text):\n    text = re.sub(r'http[s]?://(hg.mozilla|searchfox|dxr.mozilla)\\S+', '__CODE_REFERENCE_URL__', text)\n    return re.sub(r'http\\S+', '__URL__', text)\n\n\ndef cleanup_fileref(text):\n    return re.sub(r'\\w+\\.py\\b|\\w+\\.json\\b|\\w+\\.js\\b|\\w+\\.jsm\\b|\\w+\\.html\\b|\\w+\\.css\\b|\\w+\\.c\\b|\\w+\\.cpp\\b|\\w+\\.h\\b', '__FILE_REFERENCE__', text)\n\n\ndef cleanup_responses(text):\n    return re.sub('>[^\\n]+', ' ', text)\n\n\ndef cleanup_hex(text):\n    return re.sub(r'\\b0[xX][0-9a-fA-F]+\\b', '__HEX_NUMBER__', text)\n\n\ndef cleanup_dll(text):\n    return re.sub(r'\\w+(\\.dll|\\.so|\\.dylib)\\b', '__DLL_NAME__', text)\n\n\ndef cleanup_synonyms(text):\n    synonyms = [\n        ('safemode', ['safemode', 'safe mode']),\n        ('str', ['str', 'steps to reproduce', 'repro steps']),\n        ('uaf', ['uaf', 'use after free', 'use-after-free']),\n        ('asan', ['asan', 'address sanitizer', 'addresssanitizer']),\n        ('permafailure', ['permafailure', 'permafailing', 'permafail', 'perma failure', 'perma failing', 'perma fail', 'perma-failure', 'perma-failing', 'perma-fail']),\n        ('spec', ['spec', 'specification']),\n    ]\n\n    for synonym_group, synonym_list in synonyms:\n        text = re.sub('|'.join(fr'\\b{synonym}\\b' for synonym in synonym_list), synonym_group, text, flags=re.IGNORECASE)\n\n    return text\n\n\ndef cleanup_crash(text):\n    return re.sub(r'bp-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{6}[0-9]{6}\\b', '__CRASH_STATS_LINK__', text)\n\n\nclass BugExtractor(BaseEstimator, TransformerMixin):\n    def __init__(self, feature_extractors, cleanup_functions, rollback=False, rollback_when=None, commit_data=False):\n        self.feature_extractors = feature_extractors\n        self.cleanup_functions = cleanup_functions\n        self.rollback = rollback\n        self.rollback_when = rollback_when\n        self.commit_map = repository.get_commit_map() if commit_data else None\n\n    def fit(self, x, y=None):\n        return self\n\n    def transform(self, bugs):\n        results = []\n\n        for bug in bugs:\n            bug_id = bug['id']\n\n            if self.rollback:\n                bug = bug_snapshot.rollback(bug, self.rollback_when)\n\n            data = {}\n\n            if self.commit_map is not None:\n                if bug_id in self.commit_map:\n                    bug['commits'] = self.commit_map[bug_id]\n                else:\n                    bug['commits'] = []\n\n            for f in self.feature_extractors:\n                res = f(bug)\n\n                if res is None:\n                    continue\n\n                if isinstance(res, list):\n                    for item in res:\n                        data[f.__class__.__name__ + '-' + item] = 'True'\n                    continue\n\n                if isinstance(res, bool):\n                    res = str(res)\n\n                data[f.__class__.__name__] = res\n\n            # TODO: Try simply using all possible fields instead of extracting features manually.\n\n            for cleanup_function in self.cleanup_functions:\n                bug['summary'] = cleanup_function(bug['summary'])\n                for c in bug['comments']:\n                    c['text'] = cleanup_function(c['text'])\n\n            result = {\n                'data': data,\n                'title': bug['summary'],\n                'first_comment': bug['comments'][0]['text'],\n                'comments': ' '.join([c['text'] for c in bug['comments']]),\n            }\n\n            results.append(result)\n\n        return pd.DataFrame(results)\n", "path": "bugbug/bug_features.py"}]}