suoto/hdl_checker

View on GitHub
hdl_checker/static_check.py

Summary

Maintainability
B
6 hrs
Test Coverage
# This file is part of HDL Checker.
#
# Copyright (c) 2015 - 2019 suoto (Andre Souto)
#
# HDL Checker is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# HDL Checker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with HDL Checker.  If not, see <http://www.gnu.org/licenses/>.
"VHDL static checking to find unused signals, ports and constants."

import logging
import re
from typing import List, Tuple

#  from hdl_checker.path import Path
from hdl_checker.diagnostics import (
    DiagType,
    LibraryShouldBeOmited,
    ObjectIsNeverUsed,
    StaticCheckerDiag,
)

_logger = logging.getLogger(__name__)

_GET_SCOPE = re.compile(
    "|".join(
        [
            r"^\s*entity\s+(?P<entity_name>\w+)\s+is\b",
            r"^\s*architecture\s+(?P<architecture_name>\w+)\s+of\s+(?P<arch_entity>\w+)",
            r"^\s*package\s+(?P<package_name>\w+)\s+is\b",
            r"^\s*package\s+body\s+(?P<package_body_name>\w+)\s+is\b",
        ]
    ),
    flags=re.I,
).finditer

_NO_SCOPE_OBJECTS = re.compile(
    "|".join(
        [
            r"^\s*library\s+(?P<library>[\w\s,]+)",
            r"^\s*attribute\s+(?P<attribute>[\w\s,]+)\s*:",
        ]
    ),
    flags=re.I,
)

_ENTITY_OBJECTS = re.compile(
    "|".join(
        [
            r"^\s*(?P<port>[\w\s,]+)\s*:\s*(in|out|inout|buffer|linkage)\s+\w+",
            r"^\s*(?P<generic>[\w\s,]+)\s*:\s*\w+",
        ]
    ),
    flags=re.I,
).finditer

_ARCH_OBJECTS = re.compile(
    "|".join(
        [
            r"^\s*constant\s+(?P<constant>[\w\s,]+)\s*:",
            r"^\s*signal\s+(?P<signal>[\w,\s]+)\s*:",
            r"^\s*type\s+(?P<type>\w+)\s*:",
            r"^\s*shared\s+variable\s+(?P<shared_variable>[\w\s,]+)\s*:",
        ]
    ),
    flags=re.I,
).finditer

_SHOULD_END_SCAN = re.compile(
    "|".join(
        [
            r"\bgeneric\s+map",
            r"\bport\s+map",
            r"\bgenerate\b",
            r"\w+\s*:\s*entity",
            r"\bprocess\b",
        ]
    )
).search


def _getAreaFromMatch(dict_):  # pylint: disable=inconsistent-return-statements
    """
    Returns code area based on the match dict
    """
    if dict_["entity_name"] is not None:
        return "entity"
    if dict_["architecture_name"] is not None:
        return "architecture"
    if dict_["package_name"] is not None:
        return "package"
    if dict_["package_body_name"] is not None:
        return "package_body"

    assert False, "Can't determine area from {}".format(dict_)  # pragma: no cover


def _getObjectsFromText(lines):
    """
    Converts the iterator from _findObjects into a dict, whose key is the
    object's name and the value if the object's info
    """
    objects = {}
    for name, info in _findObjects(lines):
        if name not in objects:
            objects[name] = info

    return objects


def _findObjects(lines):
    """
    Returns an iterator with the object name and a dict with info about its
    location
    """
    lnum = 0
    area = None
    for _line in lines:
        line = re.sub(r"\s*--.*", "", _line)
        for match in _GET_SCOPE(line):
            area = _getAreaFromMatch(match.groupdict())

        matches = []
        if area is None:
            matches += _NO_SCOPE_OBJECTS.finditer(line)
        elif area == "entity":
            matches += _ENTITY_OBJECTS(line)
        elif area == "architecture":
            matches += _ARCH_OBJECTS(line)

        for match in matches:
            for key, value in match.groupdict().items():
                if value is None:
                    continue
                _group_d = match.groupdict()
                index = match.lastindex
                if "port" in _group_d.keys() and _group_d["port"] is not None:
                    index -= 1
                start = match.start(index)
                end = match.end(index)

                # More than 1 declaration can be done in a single line.
                # Must strip white spaces and commas properly
                for submatch in re.finditer(r"(\w+)", value):
                    # Need to decrement the last index because we have a group that
                    # catches the port type (in, out, inout, etc)
                    name = submatch.group(submatch.lastindex)
                    yield name, {
                        "lnum": lnum,
                        "start": start + submatch.start(submatch.lastindex),
                        "end": end + submatch.start(submatch.lastindex),
                        "type": key,
                    }
        lnum += 1
        if _SHOULD_END_SCAN(line):
            break


def _getUnusedObjects(lines, objects):
    """Generator that yields objects that are only found once at the
    given buffer and thus are considered unused (i.e., we only found
    its declaration"""

    text = ""
    for line in lines:
        text += re.sub(r"\s*--.*", "", line) + " "

    for _object in objects:
        r_len = 0
        single = True
        for _ in re.finditer(r"\b%s\b" % _object, text, flags=re.I):
            r_len += 1
            if r_len > 1:
                single = False
                break
        if single:
            yield _object


__COMMENT_TAG_SCANNER__ = re.compile(
    "|".join([r"\s*--\s*(?P<tag>TODO|FIXME|XXX)\s*:\s*(?P<text>.*)"])
)


def _getCommentTags(lines):
    """
    Generates diags from 'TODO', 'FIXME' and 'XXX' tags
    """
    result = []
    lnum = 0
    for line in lines:
        lnum += 1
        line_lc = line.lower()
        skip_line = True
        for tag in ("todo", "fixme", "xxx"):
            if tag in line_lc:
                skip_line = False
                break
        if skip_line:
            continue

        for match in __COMMENT_TAG_SCANNER__.finditer(line):
            _dict = match.groupdict()
            result += [
                StaticCheckerDiag(
                    line_number=lnum - 1,
                    column_number=match.start(match.lastindex - 1),
                    severity=DiagType.STYLE_INFO,
                    text="%s: %s" % (_dict["tag"].upper(), _dict["text"]),
                )
            ]
    return result


def _getMiscChecks(objects):
    """
    Get generic code hints (or it should do that sometime in the future...)
    """
    if "library" not in [x["type"] for x in objects.values()]:
        return

    for library, obj in objects.items():
        if obj["type"] != "library":
            continue
        if library == "work":
            yield LibraryShouldBeOmited(
                line_number=obj["lnum"], column_number=obj["start"], library=library
            )


def getStaticMessages(lines):
    # type: (Tuple[str, ...]) -> List[StaticCheckerDiag]
    "VHDL static checking"
    objects = _getObjectsFromText(lines)

    result = []  # type: List[StaticCheckerDiag]

    for _object in _getUnusedObjects(lines, objects.keys()):
        obj_dict = objects[_object]
        result += [
            ObjectIsNeverUsed(
                line_number=obj_dict["lnum"],
                column_number=obj_dict["start"],
                object_type=obj_dict["type"],
                object_name=_object,
            )
        ]

    return result + _getCommentTags(lines) + list(_getMiscChecks(objects))


def standalone():  # pragma: no cover
    """
    Standalone entry point
    """
    import sys

    logging.basicConfig(stream=sys.stdout, level=logging.INFO)
    for arg in sys.argv[1:]:
        print(arg)
        lines = [x.decode(errors="ignore") for x in open(arg, mode="rb").readlines()]
        for message in getStaticMessages(lines):
            print(message)
        print("=" * 10)


if __name__ == "__main__":
    standalone()