def _index_cython_code(code_lines):
    """
    Find the line position of classes and functions in *Cython* files.

    This analyzer works in a very simple way:
    It looks for the `def` and `class` keywords at zero-indentation
    level and determines the end of a class/function by the start of the
    next zero-indentation level attribute or the end of the file,
    respectively.

    By the nature of this approach, methods or inner classes are not
    identified.
    
    Parameters
    ----------
    code_lines : list of str
        The *Cython* source code splitted into lines.
    
    Returns
    -------
    line_index : dict (str -> tuple(int, int))
        Maps an attribute name to its first and last line in a Cython
        module.
    """
    line_index = {}

    for i in range(len(code_lines)):
        line = code_lines[i]
        stripped_line = line.strip()
        
        # Skip empty and comment lines
        if len(stripped_line) == 0 or stripped_line[0] == "#":
            continue
        
        if line.startswith(("def")):
            attr_type = "def"
            # Get name of the function:
            # Remove 'def' from line...
            cropped_line = stripped_line[3:].strip()
            # ...and determine the end of the name by finding the
            # subsequent '('
            cropped_line = cropped_line[:cropped_line.index("(")].strip()
            attr_name = cropped_line
        elif line.startswith(("class", "cdef class")):
            attr_type = "class"
            cropped_line = stripped_line
            # Get name of the class:
            # Remove potential 'cdef' from line...
            if cropped_line.startswith("cdef"):
                cropped_line = cropped_line[4:].strip()
            # ...and remove 'class' from line...
            cropped_line = cropped_line[5:].strip()
            # ...and determine the end of the name by finding the
            # subsequent '(' or ':'
            index = cropped_line.index("(") if "(" in cropped_line \
                    else cropped_line.index(":")
            cropped_line = cropped_line[:index].strip()
            attr_name = cropped_line
        else:
            # No new attribute -> skip line
            continue

        attr_line_start = i
        attr_line_stop = i+1
        for j in range(i+1, len(code_lines)):
            attr_line = code_lines[j]
            if len(attr_line.strip()) == 0 or attr_line.strip()[0] == "#":
                continue
            indent = len(attr_line) - len(attr_line.lstrip())
            if indent == 0:
                # No indentation -> end of attribute
                break
            else:
                # Exclusive stop -> +1
                attr_line_stop = j + 1
        
        line_index[attr_name] = (
            # 'One' based indexing
            attr_line_start + 1,
            # 'One' based indexing and inclusive stop
            attr_line_stop
        )
        
    return line_index