前言

日常工作中,有时会遇到需要删除代码中的注释或者是提取代码中的字符串,同时也是为了练习下python,所以写了这么个脚本。

分析

从源文件包含的内容解析来看,一般源代码里包含了代码、注释、字符串。其中,字符串是作为代码的一部分的。那为什么要单独分离出这么一个分类呢?主要有两点原因:其一,需求是希望有时能单独提取代码中的字符串;其二、字符串中的内容比较特殊,当处于字符串模式时,是无法进入注释模式的,分离这一点有助于简化状态机的实现。

介绍完基本的三种状态分类,我们再来考虑下状态机。状态其实就是个有向图,这个图的节点就是不同的状态,边就是从起点状态到终点状态的转移方式。基于这点,我们可以分离出比较通用的状态节点和转移方式。(ps:这里的状态图比较简单,就不画了,免得占用空间。^_^)

实现

完整代码可在这里看到,部分如下:

@unique
class StateType(Enum):
    CODE = 0
    STRING = 1
    COMMENT = 2
    ESCAPE = 3

class StateMapNote(object):
    """state-map-note.
    :param snt: type of this state note.
    :param ways: out ways of this note.
    :param name: name of note
    :param include_end: when out of this state, should include the transfer symbol
    """
    def __init__(self, snt, ways, name, include_end=False):
        self.name = name
        self.type = snt
        self.transfer_ways = ways  # k: symbols, v: StateNoteName
        self.include_end = include_end

        self.char_buffer = []

    def eat(self, c):
        if len(self.char_buffer) > 0:
            self.char_buffer.append(c)
            new_str = "".join(self.char_buffer)

            t = self._find_next_state(new_str)
            if t is not None:
                self.char_buffer.clear()
                return new_str, t
            elif self._is_likes_transfer_symbols(new_str):
                return None
            else:
                self.char_buffer.clear()
                if new_str == '**':
                    self.char_buffer.append('*')
                return new_str, self.name

        t = self._find_next_state(c)
        if t is not None:
            return c, t
        elif self._is_likes_transfer_symbols(c):
            self.char_buffer.append(c)
            return None
        else:
            return c, self.name

    def _find_next_state(self, s):
        for k, v in self.transfer_ways.items():
            if k == "" or k == s:
                return v

        return None

    def _is_likes_transfer_symbols(self, s):
        for k, v in self.transfer_ways.items():
            if k.startswith(s):
                return True
        return False


class CodeFilter(object):
    def __init__(self):
        self.cur_state = None
        self.state_map = {}

    def _find_cur_state_note(self):
        if self.cur_state in self.state_map:
            return self.state_map[self.cur_state]
        return None

    def _self_check(self):
        all_state_notes = set([x for _, y in self.state_map.items() for _, x in y.transfer_ways.items()])
        if set(self.state_map.keys()) == all_state_notes:
            return True
        return False

    def filter(self, in_file, out_file, out_type):
        if not self._self_check():
            print("self check error")
            return

        for line in in_file:
            for c in line:
                n = self._find_cur_state_note()
                if n is None:
                    print("error")
                    exit()

                ret = n.eat(c)
                # print("c is {}, ret is {}".format(c, ret))
                if ret is not None:
                    self.cur_state = ret[1]
                    csn = self._find_cur_state_note()

                    if out_type == StateType.CODE:
                        if ret[0] is not None and ((csn == n and n.type != StateType.COMMENT) or (csn != n and ((n.include_end and n.type != StateType.COMMENT) or (not n.include_end and csn.type != StateType.COMMENT)))):
                            out_file.write(ret[0])
                    elif out_type == StateType.STRING:
                        if ret[0] is not None and ((n.include_end and n.type == StateType.STRING or n.type == StateType.ESCAPE) or (not n.include_end and csn.type == StateType.STRING or csn.type == StateType.ESCAPE)):
                            out_file.write(ret[0].replace('\n', ''))
                            if (n.type == StateType.STRING or n.type == StateType.ESCAPE) and not (csn.type == StateType.STRING or csn.type == StateType.ESCAPE):
                                out_file.write('\n')
                    elif out_type == StateType.COMMENT:
                        if ret[0] is not None and ((n.include_end and n.type == StateType.COMMENT) or (not n.include_end and n.type == StateType.COMMENT)):
                            out_file.write(ret[0])

    def load_from_str(self, map_desc):
        pass


def filter_objc_source_file(in_file, out_file, out_type=StateType.CODE):
    _filter = CodeFilter()
    _filter.state_map = {
        'starter': StateMapNote(StateType.CODE, {
            '@"': "oc_string",
            "//": "single_comment",
            '"': "c_string",
            "/*": "multi_comment",
        }, "starter"),
        'oc_string': StateMapNote(StateType.STRING, {
            '"': "starter",
            '\\': "oc_escaper",
        }, "oc_string", True),
        'c_string': StateMapNote(StateType.STRING, {
            '"': "starter",
            '\\': "c_escaper",
        }, "c_string", True),
        'single_comment': StateMapNote(StateType.COMMENT, {
            "\\\n": "single_comment",
            "\n": "starter",
        }, "single_comment"),
        'multi_comment': StateMapNote(StateType.COMMENT, {
            "*/": "starter",
        }, "multi_comment", True),
        'oc_escaper': StateMapNote(StateType.ESCAPE, {
            "": "oc_string"
        }, "oc_escaper"),
        'c_escaper': StateMapNote(StateType.ESCAPE, {
            "": "c_string"
        }, "c_escaper")
    }

    _filter.cur_state = "starter"

    _filter.filter(in_file, out_file, out_type)

首先是对源代码内容类型的分类,里面比刚才分析提到的多了一种ESCAPE模式,这是用于字符串内部转义使用的,也是为了简化状态机的一种方式。

代码实现上,首先用StateMapNote定义了状态节点,这个节点包含了对状态转移的处理。每个节点拥有自己的type和名字,同时转移方式里也包含了对应的节点的名字,这个名字是唯一的。CodeFilter这是用来管理这些节点,用一个state_map来管理这些状态节点。同时负责从文件中读取内容进行处理,并将结果保存到输出文件当中。这个CodeFilter灵活的一点在于状态图是可以通过简单的方式自定义的。

检验

通过使用这个脚本,再配合另外一个脚本,尝试将工作的项目(挺大的一个iOS项目)进行了注释过滤,在修复两个小问题后,成功地运行了起来,通过随机抽查源代码的方式,发现注释都被去掉了。(提取字符串没有经过这种检验,只是简单的测试了下。)

遇到的两个问题是:

1、形如这种:int/*comment*/var; 的代码在经过去注释后变成了 intvar; 导致编不过。这种写法从代码风格上来讲是很不好的,所以也就没打算处理这种情况了。

2、另一个脚本的问题,复制项目的时候空文件夹没有复制,导致编译的时候缺少对应文件夹。这个也暂时没有处理,通过手动创建文件夹修复的。当然,这个问题和本文所提的脚本无关。

缺点

因为笔者是从iOS相关的工作,所以这个脚本设计之初就是针对Objective-C、C、C++的代码做处理,所以从设计到实现再到检验,都是从类C语言考量的,适用性不是很广。有兴趣的朋友可以自己测试修改。

结语

从代码设计的角度来看,虽然实现了状态图比较方便的自定义。但是还是存在一些优化的点,比如每个状态的处理逻辑都是一致的且写在了StateMapNote里,其实可以通过一些接口实现或者继承的方式来定义不同的处理行为。虽然可能说对于一个脚本来说继承这些显得过于复杂,但是多思考还是没坏处的。不断地思考总结,才能提升代码水平。

以上。