From 040ad4828459bc5ccfb4f2527da287dc175a53bb Mon Sep 17 00:00:00 2001 From: Stanislav Pankevich Date: Fri, 22 Nov 2024 22:52:04 +0100 Subject: [PATCH] backend/sdoc_source_code: recognize C++ class function declarations/definitions --- pyproject.toml | 2 +- .../sdoc_source_code/caching_reader.py | 9 +- .../backend/sdoc_source_code/reader_c.py | 110 ++++++-- .../sdoc_source_code/tree_sitter_helpers.py | 11 +- .../file.hpp | 6 + .../input.sdoc | 10 + .../strictdoc.toml | 7 + .../test.itest | 15 + .../file.cpp | 3 + .../file.hpp | 6 + .../input.sdoc | 10 + .../strictdoc.toml | 7 + .../test.itest | 16 ++ .../test_dsl_source_file_syntax_cpp.py | 256 ++++++++++++++++++ 14 files changed, 447 insertions(+), 21 deletions(-) create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/file.hpp create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/input.sdoc create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/strictdoc.toml create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/test.itest create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.cpp create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.hpp create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/input.sdoc create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/strictdoc.toml create mode 100644 tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/test.itest create mode 100644 tests/unit/strictdoc/backend/sdoc_source_code/test_dsl_source_file_syntax_cpp.py diff --git a/pyproject.toml b/pyproject.toml index 51fbb2003..3ad8f1f8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ dependencies = [ # Tree Sitter is used for language/AST-aware parsing of Python, C and other files. "tree-sitter", - "tree-sitter-c", + "tree_sitter_cpp", "tree-sitter-python", # Requirements-to-source traceability. Colored syntax for source files. diff --git a/strictdoc/backend/sdoc_source_code/caching_reader.py b/strictdoc/backend/sdoc_source_code/caching_reader.py index aa02a7875..dc0ae306a 100644 --- a/strictdoc/backend/sdoc_source_code/caching_reader.py +++ b/strictdoc/backend/sdoc_source_code/caching_reader.py @@ -54,6 +54,13 @@ def _get_reader( if project_config.is_activated_source_file_language_parsers(): if path_to_file.endswith(".py"): return SourceFileTraceabilityReader_Python() - if path_to_file.endswith(".c") or path_to_file.endswith(".h"): + if ( + path_to_file.endswith(".c") + or path_to_file.endswith(".cc") + or path_to_file.endswith(".h") + or path_to_file.endswith(".hh") + or path_to_file.endswith(".hpp") + or path_to_file.endswith(".cpp") + ): return SourceFileTraceabilityReader_C() return SourceFileTraceabilityReader() diff --git a/strictdoc/backend/sdoc_source_code/reader_c.py b/strictdoc/backend/sdoc_source_code/reader_c.py index 3defac162..82f0bc779 100644 --- a/strictdoc/backend/sdoc_source_code/reader_c.py +++ b/strictdoc/backend/sdoc_source_code/reader_c.py @@ -1,9 +1,9 @@ # mypy: disable-error-code="no-redef,no-untyped-call,no-untyped-def,type-arg,var-annotated" import sys import traceback -from typing import List, Optional, Union +from typing import List, Optional, Sequence, Union -import tree_sitter_c +import tree_sitter_cpp from tree_sitter import Language, Node, Parser from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError @@ -12,6 +12,7 @@ from strictdoc.backend.sdoc_source_code.models.function import Function from strictdoc.backend.sdoc_source_code.models.function_range_marker import ( FunctionRangeMarker, + RangeMarkerType, ) from strictdoc.backend.sdoc_source_code.models.range_marker import ( LineMarker, @@ -52,7 +53,7 @@ def read( parse_context = ParseContext(file_path, length) # Works since Python 3.9 but we also lint this with mypy from Python 3.8. - language_arg = tree_sitter_c.language() + language_arg = tree_sitter_cpp.language() py_language = Language( # type: ignore[call-arg, unused-ignore] language_arg ) @@ -82,6 +83,12 @@ def read( node_.start_point[1] + 1, ) for marker_ in markers: + if not isinstance(marker_, FunctionRangeMarker): + continue + # At the top level, only accept the scope=file markers. + # Everything else will be handled by functions and classes. + if marker_.scope != RangeMarkerType.FILE: + continue if isinstance(marker_, FunctionRangeMarker) and ( function_range_marker_ := marker_ ): @@ -91,15 +98,40 @@ def read( traceability_info.markers.append( function_range_marker_ ) - elif node_.type == "declaration": + + elif node_.type in ("declaration", "field_declaration"): function_declarator_node = ts_find_child_node_by_type( node_, "function_declarator" ) + + # C++ reference declaration wrap the function declaration one time. if function_declarator_node is None: - continue + # Example: TrkVertex& operator-=(const TrkVertex& c); + reference_declarator_node = ts_find_child_node_by_type( + node_, "reference_declarator" + ) + if reference_declarator_node is None: + continue + function_declarator_node = ts_find_child_node_by_type( + reference_declarator_node, "function_declarator" + ) + if function_declarator_node is None: + continue + + # For normal C functions the identifier is "identifier". + # For C++, there are: + # Class function declarations: bool CanSend(const CanFrame &frame); # noqa: ERA001 + # Operators: TrkVertex& operator-=(const TrkVertex& c); # noqa: ERA001 + # Destructors: ~TrkVertex(); # noqa: ERA001 function_identifier_node = ts_find_child_node_by_type( - function_declarator_node, "identifier" + function_declarator_node, + node_type=( + "identifier", + "field_identifier", + "operator_name", + "destructor_name", + ), ) if function_identifier_node is None: continue @@ -110,7 +142,15 @@ def read( function_name: str = function_identifier_node.text.decode( "utf8" ) - assert function_name is not None, function_name + assert ( + function_name is not None + ), "function_name must not be None" + + parent_names = self.get_node_ns(node_) + if len(parent_names) > 0: + function_name = ( + f"{'::'.join(parent_names)}::{function_name}" + ) function_attributes = {FunctionAttribute.DECLARATION} for specifier_node_ in ts_find_child_nodes_by_type( @@ -173,11 +213,28 @@ def read( for child_ in node_.children: if child_.type == "function_declarator": - assert child_.children[0].type == "identifier" - assert child_.children[0].text - - function_name = child_.children[0].text.decode("utf8") - assert function_name is not None, "Function name" + identifier_node = ts_find_child_node_by_type( + child_, + ( + "identifier", + "qualified_identifier", + "destructor_name", + ), + ) + if identifier_node is None: + raise NotImplementedError(child_) + + assert identifier_node.text is not None + function_name = identifier_node.text.decode("utf8") + + assert ( + function_name is not None + ), "Could not parse function name" + parent_names = self.get_node_ns(node_) + if len(parent_names) > 0: + function_name = ( + f"{'::'.join(parent_names)}::{function_name}" + ) function_markers: List[FunctionRangeMarker] = [] function_comment_node: Optional[Node] = None @@ -187,7 +244,9 @@ def read( and node_.prev_sibling.type == "comment" ): function_comment_node = node_.prev_sibling - assert function_comment_node.text is not None + assert ( + function_comment_node.text is not None + ), function_comment_node function_comment_text = function_comment_node.text.decode( "utf8" ) @@ -287,9 +346,28 @@ def read_from_file(self, file_path): sys.exit(1) except Exception as exc: # pylint: disable=broad-except print( # noqa: T201 - f"error: SourceFileTraceabilityReader_Python: could not parse file: " + f"error: SourceFileTraceabilityReader_C: could not parse file: " f"{file_path}.\n{exc.__class__.__name__}: {exc}" ) - # TODO: when --debug is provided - # traceback.print_exc() # noqa: ERA001 + traceback.print_exc() sys.exit(1) + + @staticmethod + def get_node_ns(node: Node) -> Sequence[str]: + """Walk up the tree and find parent classes""" + parent_scopes = [] + cursor: Optional[Node] = node + while cursor is not None: + if cursor.type == "class_specifier" and len(cursor.children) > 1: + second_node_or_none = cursor.children[1] + if ( + second_node_or_none.type == "type_identifier" + and second_node_or_none.text is not None + ): + parent_class_name = second_node_or_none.text.decode("utf8") + parent_scopes.append(parent_class_name) + + cursor = cursor.parent + + parent_scopes.reverse() + return parent_scopes diff --git a/strictdoc/backend/sdoc_source_code/tree_sitter_helpers.py b/strictdoc/backend/sdoc_source_code/tree_sitter_helpers.py index 6bde2129e..3e94be729 100644 --- a/strictdoc/backend/sdoc_source_code/tree_sitter_helpers.py +++ b/strictdoc/backend/sdoc_source_code/tree_sitter_helpers.py @@ -1,4 +1,4 @@ -from typing import Generator, Optional +from typing import Generator, Optional, Tuple, Union from tree_sitter import Node, Tree @@ -18,9 +18,14 @@ def traverse_tree(tree: Tree) -> Generator[Node, None, None]: break -def ts_find_child_node_by_type(node: Node, node_type: str) -> Optional[Node]: +def ts_find_child_node_by_type( + node: Node, node_type: Union[str, Tuple[str, ...], str] +) -> Optional[Node]: + node_types: Tuple[str, ...] = ( + node_type if isinstance(node_type, tuple) else (node_type,) + ) for child_ in node.children: - if child_.type == node_type: + if child_.type in node_types: return child_ return None diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/file.hpp b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/file.hpp new file mode 100644 index 000000000..e9d6dc7d4 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/file.hpp @@ -0,0 +1,6 @@ +class EcuHal +{ + public: + // @relation(REQ-1, scope=function) + bool CanSend(const CanFrame &frame); +}; diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/input.sdoc b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/input.sdoc new file mode 100644 index 000000000..e59e74283 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/input.sdoc @@ -0,0 +1,10 @@ +[DOCUMENT] +TITLE: Hello world doc + +[REQUIREMENT] +UID: REQ-1 +TITLE: Requirement Title +STATEMENT: Requirement Statement +RELATIONS: +- TYPE: File + VALUE: file.hpp diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/strictdoc.toml b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/strictdoc.toml new file mode 100644 index 000000000..f5502ad57 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/strictdoc.toml @@ -0,0 +1,7 @@ +[project] + +features = [ + "REQUIREMENT_TO_SOURCE_TRACEABILITY", + "SOURCE_FILE_LANGUAGE_PARSERS", + "PROJECT_STATISTICS_SCREEN" +] diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/test.itest b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/test.itest new file mode 100644 index 000000000..045a1e9d5 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/01_cpp_class_function_declaration/test.itest @@ -0,0 +1,15 @@ +REQUIRES: PYTHON_39_OR_HIGHER + +RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail +CHECK: Published: Hello world doc + +RUN: %check_exists --file "%S/Output/html/_source_files/file.hpp.html" + +RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML +CHECK-HTML: + +RUN: %cat %S/Output/html/_source_files/file.hpp.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE +CHECK-SOURCE-FILE: + +RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-COVERAGE +CHECK-SOURCE-COVERAGE: 33.3% diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.cpp b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.cpp new file mode 100644 index 000000000..798013ae6 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.cpp @@ -0,0 +1,3 @@ +bool EcuHal::CanSend(const CanFrame &frame) { + return true; +} diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.hpp b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.hpp new file mode 100644 index 000000000..e9d6dc7d4 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/file.hpp @@ -0,0 +1,6 @@ +class EcuHal +{ + public: + // @relation(REQ-1, scope=function) + bool CanSend(const CanFrame &frame); +}; diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/input.sdoc b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/input.sdoc new file mode 100644 index 000000000..e59e74283 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/input.sdoc @@ -0,0 +1,10 @@ +[DOCUMENT] +TITLE: Hello world doc + +[REQUIREMENT] +UID: REQ-1 +TITLE: Requirement Title +STATEMENT: Requirement Statement +RELATIONS: +- TYPE: File + VALUE: file.hpp diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/strictdoc.toml b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/strictdoc.toml new file mode 100644 index 000000000..f5502ad57 --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/strictdoc.toml @@ -0,0 +1,7 @@ +[project] + +features = [ + "REQUIREMENT_TO_SOURCE_TRACEABILITY", + "SOURCE_FILE_LANGUAGE_PARSERS", + "PROJECT_STATISTICS_SCREEN" +] diff --git a/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/test.itest b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/test.itest new file mode 100644 index 000000000..1ee8273bd --- /dev/null +++ b/tests/integration/features/file_traceability/_language_parsers/cpp/02_cpp_class_function_declaration_and_definition/test.itest @@ -0,0 +1,16 @@ +REQUIRES: PYTHON_39_OR_HIGHER + +RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail +CHECK: Published: Hello world doc + +RUN: %check_exists --file "%S/Output/html/_source_files/file.hpp.html" + +RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML +CHECK-HTML: +CHECK-HTML: + +RUN: %cat %S/Output/html/_source_files/file.hpp.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE +CHECK-SOURCE-FILE: + +RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-COVERAGE +CHECK-SOURCE-COVERAGE: 33.3% diff --git a/tests/unit/strictdoc/backend/sdoc_source_code/test_dsl_source_file_syntax_cpp.py b/tests/unit/strictdoc/backend/sdoc_source_code/test_dsl_source_file_syntax_cpp.py new file mode 100644 index 000000000..35464327b --- /dev/null +++ b/tests/unit/strictdoc/backend/sdoc_source_code/test_dsl_source_file_syntax_cpp.py @@ -0,0 +1,256 @@ +import sys + +import pytest + +from strictdoc.backend.sdoc_source_code.models.function import Function +from strictdoc.backend.sdoc_source_code.models.function_range_marker import ( + FunctionRangeMarker, +) +from strictdoc.backend.sdoc_source_code.models.source_file_info import ( + SourceFileTraceabilityInfo, +) +from strictdoc.backend.sdoc_source_code.reader_c import ( + SourceFileTraceabilityReader_C, +) + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 9), reason="Requires Python 3.9 or higher" +) + + +def test_01_class_function_declaration(): + input_string = b"""\ +class Foo +{ + public: + /** + * @relation(REQ-1, scope=function) + */ + bool CanSend(const CanFrame &frame); +}; +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 1 + assert len(info.markers) == 1 + + function: Function = info.functions[0] + assert function.name == "Foo::CanSend" + assert function.line_begin == 4 + assert function.line_end == 7 + + marker: FunctionRangeMarker = info.markers[0] + assert marker.ng_range_line_begin == 4 + assert marker.ng_range_line_end == 7 + + +def test_02_nested_class_function_declaration(): + input_string = b"""\ +class Foo { +class Bar { + public: + /** + * @relation(REQ-1, scope=function) + */ + bool CanSend(const CanFrame &frame); +}; +}; +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 1 + assert len(info.markers) == 1 + + function: Function = info.functions[0] + assert function.name == "Foo::Bar::CanSend" + assert function.line_begin == 4 + assert function.line_end == 7 + + marker: FunctionRangeMarker = info.markers[0] + assert marker.ng_range_line_begin == 4 + assert marker.ng_range_line_end == 7 + + +def test_11_class_function_definition(): + input_string = b"""\ +/** + * @relation(REQ-1, scope=function) + */ +bool Foo::Bar::CanSend(const CanFrame &frame) { + return true; +} +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 1 + assert len(info.markers) == 1 + + function: Function = info.functions[0] + assert function.name == "Foo::Bar::CanSend" + assert function.line_begin == 1 + assert function.line_end == 6 + + marker: FunctionRangeMarker = info.markers[0] + assert marker.ng_range_line_begin == 1 + assert marker.ng_range_line_end == 6 + + +def test_12_class_function_declaration_returning_reference(): + input_string = b"""\ +class TrkVertex +{ + public: + double x_ = 0; //!< mm, x axis pointing right + double y_ = 0; //!< mm, y axis pointing up + + /** + * @relation(REQ-1, scope=function) + */ + TrkVertex& operator-=(const TrkVertex& c); +}; +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 1 + assert len(info.markers) == 1 + + function: Function = info.functions[0] + assert function.name == "TrkVertex::operator-=" + assert function.line_begin == 7 + assert function.line_end == 10 + + marker: FunctionRangeMarker = info.markers[0] + assert marker.ng_range_line_begin == 7 + assert marker.ng_range_line_end == 10 + + +def test_20_constructor_and_destructor_declarations(): + input_string = b"""\ +class TrkVertex +{ + public: + /** + * @relation(REQ-1, scope=function) + */ + TrkVertex(); + + /** + * @relation(REQ-1, scope=function) + */ + TrkVertex(double x, double y); + + /** + * @relation(REQ-1, scope=function) + */ + ~TrkVertex(); +}; +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 3 + assert len(info.markers) == 3 + + function_1: Function = info.functions[0] + assert function_1.name == "TrkVertex::TrkVertex" + assert function_1.line_begin == 4 + assert function_1.line_end == 7 + + function_2: Function = info.functions[1] + assert function_2.name == "TrkVertex::TrkVertex" + assert function_2.line_begin == 9 + assert function_2.line_end == 12 + + function_3: Function = info.functions[2] + assert function_3.name == "TrkVertex::~TrkVertex" + assert function_3.line_begin == 14 + assert function_3.line_end == 17 + + marker_1: FunctionRangeMarker = info.markers[0] + assert marker_1.ng_range_line_begin == 4 + assert marker_1.ng_range_line_end == 7 + + marker_2: FunctionRangeMarker = info.markers[1] + assert marker_2.ng_range_line_begin == 9 + assert marker_2.ng_range_line_end == 12 + + marker_3: FunctionRangeMarker = info.markers[2] + assert marker_3.ng_range_line_begin == 14 + assert marker_3.ng_range_line_end == 17 + + +def test_21_constructor_and_destructor_definitions(): + input_string = b"""\ +class TrkVertex +{ + public: + /** + * @relation(REQ-1, scope=function) + */ + TrkVertex() {} + + /** + * @relation(REQ-1, scope=function) + */ + TrkVertex(double x, double y) {} + + /** + * @relation(REQ-1, scope=function) + */ + ~TrkVertex() {} +}; +""" + + reader = SourceFileTraceabilityReader_C() + + info = reader.read(input_string) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.functions) == 3 + assert len(info.markers) == 3 + + function_1: Function = info.functions[0] + assert function_1.name == "TrkVertex::TrkVertex" + assert function_1.line_begin == 4 + assert function_1.line_end == 7 + + function_2: Function = info.functions[1] + assert function_2.name == "TrkVertex::TrkVertex" + assert function_2.line_begin == 9 + assert function_2.line_end == 12 + + function_3: Function = info.functions[2] + assert function_3.name == "TrkVertex::~TrkVertex" + assert function_3.line_begin == 14 + assert function_3.line_end == 17 + + marker_1: FunctionRangeMarker = info.markers[0] + assert marker_1.ng_range_line_begin == 4 + assert marker_1.ng_range_line_end == 7 + + marker_2: FunctionRangeMarker = info.markers[1] + assert marker_2.ng_range_line_begin == 9 + assert marker_2.ng_range_line_end == 12 + + marker_3: FunctionRangeMarker = info.markers[2] + assert marker_3.ng_range_line_begin == 14 + assert marker_3.ng_range_line_end == 17