1# Copyright (c) Facebook, Inc. and its affiliates.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
5
6import dataclasses
7from typing import Union
8
9from libcst._parser.detect_config import detect_config
10from libcst._parser.parso.utils import PythonVersionInfo
11from libcst._parser.types.config import ParserConfig, PartialParserConfig
12from libcst.testing.utils import UnitTest, data_provider
13
14
15class TestDetectConfig(UnitTest):
16    @data_provider(
17        {
18            "empty_input": {
19                "source": b"",
20                "partial": PartialParserConfig(python_version="3.7"),
21                "detect_trailing_newline": True,
22                "detect_default_newline": True,
23                "expected_config": ParserConfig(
24                    lines=["\n", ""],
25                    encoding="utf-8",
26                    default_indent="    ",
27                    default_newline="\n",
28                    has_trailing_newline=False,
29                    version=PythonVersionInfo(3, 7),
30                    future_imports=frozenset(),
31                ),
32            },
33            "detect_trailing_newline_disabled": {
34                "source": b"",
35                "partial": PartialParserConfig(python_version="3.7"),
36                "detect_trailing_newline": False,
37                "detect_default_newline": True,
38                "expected_config": ParserConfig(
39                    lines=[""],  # the trailing newline isn't inserted
40                    encoding="utf-8",
41                    default_indent="    ",
42                    default_newline="\n",
43                    has_trailing_newline=False,
44                    version=PythonVersionInfo(3, 7),
45                    future_imports=frozenset(),
46                ),
47            },
48            "detect_default_newline_disabled": {
49                "source": b"pass\r",
50                "partial": PartialParserConfig(python_version="3.7"),
51                "detect_trailing_newline": False,
52                "detect_default_newline": False,
53                "expected_config": ParserConfig(
54                    lines=["pass\r", ""],  # the trailing newline isn't inserted
55                    encoding="utf-8",
56                    default_indent="    ",
57                    default_newline="\n",
58                    has_trailing_newline=False,
59                    version=PythonVersionInfo(3, 7),
60                    future_imports=frozenset(),
61                ),
62            },
63            "newline_inferred": {
64                "source": b"first_line\r\n\nsomething\n",
65                "partial": PartialParserConfig(python_version="3.7"),
66                "detect_trailing_newline": True,
67                "detect_default_newline": True,
68                "expected_config": ParserConfig(
69                    lines=["first_line\r\n", "\n", "something\n", ""],
70                    encoding="utf-8",
71                    default_indent="    ",
72                    default_newline="\r\n",
73                    has_trailing_newline=True,
74                    version=PythonVersionInfo(3, 7),
75                    future_imports=frozenset(),
76                ),
77            },
78            "newline_partial_given": {
79                "source": b"first_line\r\nsecond_line\r\n",
80                "partial": PartialParserConfig(
81                    default_newline="\n", python_version="3.7"
82                ),
83                "detect_trailing_newline": True,
84                "detect_default_newline": True,
85                "expected_config": ParserConfig(
86                    lines=["first_line\r\n", "second_line\r\n", ""],
87                    encoding="utf-8",
88                    default_indent="    ",
89                    default_newline="\n",  # The given partial disables inference
90                    has_trailing_newline=True,
91                    version=PythonVersionInfo(3, 7),
92                    future_imports=frozenset(),
93                ),
94            },
95            "indent_inferred": {
96                "source": b"if test:\n\t  something\n",
97                "partial": PartialParserConfig(python_version="3.7"),
98                "detect_trailing_newline": True,
99                "detect_default_newline": True,
100                "expected_config": ParserConfig(
101                    lines=["if test:\n", "\t  something\n", ""],
102                    encoding="utf-8",
103                    default_indent="\t  ",
104                    default_newline="\n",
105                    has_trailing_newline=True,
106                    version=PythonVersionInfo(3, 7),
107                    future_imports=frozenset(),
108                ),
109            },
110            "indent_partial_given": {
111                "source": b"if test:\n\t  something\n",
112                "partial": PartialParserConfig(
113                    default_indent="      ", python_version="3.7"
114                ),
115                "detect_trailing_newline": True,
116                "detect_default_newline": True,
117                "expected_config": ParserConfig(
118                    lines=["if test:\n", "\t  something\n", ""],
119                    encoding="utf-8",
120                    default_indent="      ",
121                    default_newline="\n",
122                    has_trailing_newline=True,
123                    version=PythonVersionInfo(3, 7),
124                    future_imports=frozenset(),
125                ),
126            },
127            "encoding_inferred": {
128                "source": b"#!/usr/bin/python3\n# -*- coding: latin-1 -*-\npass\n",
129                "partial": PartialParserConfig(python_version="3.7"),
130                "detect_trailing_newline": True,
131                "detect_default_newline": True,
132                "expected_config": ParserConfig(
133                    lines=[
134                        "#!/usr/bin/python3\n",
135                        "# -*- coding: latin-1 -*-\n",
136                        "pass\n",
137                        "",
138                    ],
139                    encoding="iso-8859-1",  # this is an alias for latin-1
140                    default_indent="    ",
141                    default_newline="\n",
142                    has_trailing_newline=True,
143                    version=PythonVersionInfo(3, 7),
144                    future_imports=frozenset(),
145                ),
146            },
147            "encoding_partial_given": {
148                "source": b"#!/usr/bin/python3\n# -*- coding: latin-1 -*-\npass\n",
149                "partial": PartialParserConfig(
150                    encoding="us-ascii", python_version="3.7"
151                ),
152                "detect_trailing_newline": True,
153                "detect_default_newline": True,
154                "expected_config": ParserConfig(
155                    lines=[
156                        "#!/usr/bin/python3\n",
157                        "# -*- coding: latin-1 -*-\n",
158                        "pass\n",
159                        "",
160                    ],
161                    encoding="us-ascii",
162                    default_indent="    ",
163                    default_newline="\n",
164                    has_trailing_newline=True,
165                    version=PythonVersionInfo(3, 7),
166                    future_imports=frozenset(),
167                ),
168            },
169            "encoding_str_not_bytes_disables_inference": {
170                "source": "#!/usr/bin/python3\n# -*- coding: latin-1 -*-\npass\n",
171                "partial": PartialParserConfig(python_version="3.7"),
172                "detect_trailing_newline": True,
173                "detect_default_newline": True,
174                "expected_config": ParserConfig(
175                    lines=[
176                        "#!/usr/bin/python3\n",
177                        "# -*- coding: latin-1 -*-\n",
178                        "pass\n",
179                        "",
180                    ],
181                    encoding="utf-8",  # because source is a str, don't infer latin-1
182                    default_indent="    ",
183                    default_newline="\n",
184                    has_trailing_newline=True,
185                    version=PythonVersionInfo(3, 7),
186                    future_imports=frozenset(),
187                ),
188            },
189            "encoding_non_ascii_compatible_utf_16_with_bom": {
190                "source": b"\xff\xfet\x00e\x00s\x00t\x00",
191                "partial": PartialParserConfig(encoding="utf-16", python_version="3.7"),
192                "detect_trailing_newline": True,
193                "detect_default_newline": True,
194                "expected_config": ParserConfig(
195                    lines=["test\n", ""],
196                    encoding="utf-16",
197                    default_indent="    ",
198                    default_newline="\n",
199                    has_trailing_newline=False,
200                    version=PythonVersionInfo(3, 7),
201                    future_imports=frozenset(),
202                ),
203            },
204            "detect_trailing_newline_missing_newline": {
205                "source": b"test",
206                "partial": PartialParserConfig(python_version="3.7"),
207                "detect_trailing_newline": True,
208                "detect_default_newline": True,
209                "expected_config": ParserConfig(
210                    lines=["test\n", ""],
211                    encoding="utf-8",
212                    default_indent="    ",
213                    default_newline="\n",
214                    has_trailing_newline=False,
215                    version=PythonVersionInfo(3, 7),
216                    future_imports=frozenset(),
217                ),
218            },
219            "detect_trailing_newline_has_newline": {
220                "source": b"test\n",
221                "partial": PartialParserConfig(python_version="3.7"),
222                "detect_trailing_newline": True,
223                "detect_default_newline": True,
224                "expected_config": ParserConfig(
225                    lines=["test\n", ""],
226                    encoding="utf-8",
227                    default_indent="    ",
228                    default_newline="\n",
229                    has_trailing_newline=True,
230                    version=PythonVersionInfo(3, 7),
231                    future_imports=frozenset(),
232                ),
233            },
234            "detect_trailing_newline_missing_newline_after_line_continuation": {
235                "source": b"test\\\n",
236                "partial": PartialParserConfig(python_version="3.7"),
237                "detect_trailing_newline": True,
238                "detect_default_newline": True,
239                "expected_config": ParserConfig(
240                    lines=["test\\\n", "\n", ""],
241                    encoding="utf-8",
242                    default_indent="    ",
243                    default_newline="\n",
244                    has_trailing_newline=False,
245                    version=PythonVersionInfo(3, 7),
246                    future_imports=frozenset(),
247                ),
248            },
249            "detect_trailing_newline_has_newline_after_line_continuation": {
250                "source": b"test\\\n\n",
251                "partial": PartialParserConfig(python_version="3.7"),
252                "detect_trailing_newline": True,
253                "detect_default_newline": True,
254                "expected_config": ParserConfig(
255                    lines=["test\\\n", "\n", ""],
256                    encoding="utf-8",
257                    default_indent="    ",
258                    default_newline="\n",
259                    has_trailing_newline=True,
260                    version=PythonVersionInfo(3, 7),
261                    future_imports=frozenset(),
262                ),
263            },
264            "future_imports_in_correct_position": {
265                "source": b"# C\n''' D '''\nfrom __future__ import a as b\n",
266                "partial": PartialParserConfig(python_version="3.7"),
267                "detect_trailing_newline": True,
268                "detect_default_newline": True,
269                "expected_config": ParserConfig(
270                    lines=[
271                        "# C\n",
272                        "''' D '''\n",
273                        "from __future__ import a as b\n",
274                        "",
275                    ],
276                    encoding="utf-8",
277                    default_indent="    ",
278                    default_newline="\n",
279                    has_trailing_newline=True,
280                    version=PythonVersionInfo(3, 7),
281                    future_imports=frozenset({"a"}),
282                ),
283            },
284            "future_imports_in_mixed_position": {
285                "source": (
286                    b"from __future__ import a, b\nimport os\n"
287                    + b"from __future__ import c\n"
288                ),
289                "partial": PartialParserConfig(python_version="3.7"),
290                "detect_trailing_newline": True,
291                "detect_default_newline": True,
292                "expected_config": ParserConfig(
293                    lines=[
294                        "from __future__ import a, b\n",
295                        "import os\n",
296                        "from __future__ import c\n",
297                        "",
298                    ],
299                    encoding="utf-8",
300                    default_indent="    ",
301                    default_newline="\n",
302                    has_trailing_newline=True,
303                    version=PythonVersionInfo(3, 7),
304                    future_imports=frozenset({"a", "b"}),
305                ),
306            },
307        }
308    )
309    def test_detect_module_config(
310        self,
311        *,
312        source: Union[str, bytes],
313        partial: PartialParserConfig,
314        detect_trailing_newline: bool,
315        detect_default_newline: bool,
316        expected_config: ParserConfig,
317    ) -> None:
318        self.assertEqual(
319            dataclasses.asdict(
320                detect_config(
321                    source,
322                    partial=partial,
323                    detect_trailing_newline=detect_trailing_newline,
324                    detect_default_newline=detect_default_newline,
325                ).config
326            ),
327            dataclasses.asdict(expected_config),
328        )
329