针对列表项错误之间缺少逗号发出警告

Issue warning for missing comma between list items bug

故事:

当在多行上定义字符串列表时,通常很容易忘记列表项之间的逗号,如本例所示:

test = [
    "item1"
    "item2"
]

列表 test 现在只有一个项目 "item1item2"

问题经常出现在重新排列列表中的项目后。

有此问题的示例堆栈溢出问题:

问题:

有没有办法,最好使用静态代码分析,在这种情况下发出警告以便尽早发现问题?

这些只是可能的解决方案,因为我不太适合 static-analysis

tokenize:

我最近摆弄了 with tokenizing python code,我相信在添加足够的逻辑后,它具有执行此类检查所需的所有信息。对于您给定的列表,使用 python -m tokenize list1.py 生成的标记如下:

python -m tokenize list1.py 

1,0-1,4:    NAME    'test'
1,5-1,6:    OP  '='
1,7-1,8:    OP  '['
1,8-1,9:    NL  '\n'
2,1-2,8:    STRING  '"item1"'
2,8-2,9:    NL  '\n'
3,1-3,8:    STRING  '"item2"'
3,8-3,9:    NL  '\n'
4,0-4,1:    OP  ']'
4,1-4,2:    NEWLINE '\n'
5,0-5,0:    ENDMARKER   ''

这当然是“有问题的”情况,内容将被串联起来。在 , 存在的情况下,输出略有变化以反映这一点(我仅为列表主体添加了标记):

1,7-1,8:    OP  '['
1,8-1,9:    NL  '\n'
2,1-2,8:    STRING  '"item1"'
2,8-2,9:    OP  ','
2,9-2,10:   NL  '\n'
3,1-3,8:    STRING  '"item2"'
3,8-3,9:    NL  '\n'
4,0-4,1:    OP  ']'

现在我们有了额外的 OP ',' 标记,表示存在以逗号分隔的第二个元素。

根据这些信息,我们可以使用 tokenize 模块中非常方便的方法 generate_tokens。方法tokenize.generate_tokens() , tokenize.tokenize() in Py3, has a single argument readline, a method on file-like objects which essentially returns the next line for that file like object ()。它 returns 一个命名的元组,总共有 5 个元素,包含有关标记类型的信息、标记字符串以及行号和行中的位置。

使用此信息,理论上可以循环遍历文件,并且当列表初始化中不存在 OP ',' 时(通过检查标记 NAMEOP '='OP '[' 存在于同一行号),可以在检测到它的行上发出警告。

这种方法的好处是它很容易 straight-forward 概括。为了适应发生字符串文字连接的所有情况(即,在 'grouping' 运算符 (), {}, [] 内),您检查令牌是否属于 type = 51 (or 53 for Python 3)或任何 [=29 中的值=] 存在于同一条线上(这些是粗略的,最重要的建议 atm)。

现在,我不太确定其他人是如何处理这些问题的 但是 它看起来它可能是你可以研究的东西tokenize 提供了所有必要的信息,唯一缺少的是检测它的逻辑。

实施注意事项:这些值(例如,type)在不同版本之间确实有所不同,并且可能会发生变化,因此应该注意这一点。不过,人们可能会利用这个 by only working with constants 作为代币。


parserast:

另一个可能更乏味的解决方案可能涉及 parser and ast 模块。字符串的连接实际上是在抽象语法树的创建过程中执行的,因此您也可以在那里检测它。

我真的不想转储我要提到的 parserast 方法的完整输出,但是,只是为了确保我们在同一页,我将使用以下列表初始化语句:

l_init = """
test = [
    "item1"
    "item2",
    "item3"
]
"""

为了生成解析树,使用p = parser.suite(l_init). After this is done, you can get a view of it with p.tolist()(输出太大无法添加)。您注意到 三个不同的 str 对象将有三个条目 item1item2item3.

另一方面,当使用 node = ast.parse(l_init) and viewed with ast.dump(node) 创建 AST 时,只有两个条目 :一个用于连接 strs item1item2 和另一个条目 item3.

所以,这是另一种可行的方法,但正如我之前提到的,它更乏味。我不确定行信息是否可用并且您处理两个不同的模块。如果您可能想在编译器链中较高的位置使用内部对象,请将其作为后顾之忧。


结束评论:作为结束语,在这种情况下,tokenize 方法似乎是最合乎逻辑的方法。相反,似乎 pylint 实际上适用于 astroid a python lib that eases analysis of Abstract Syntax Trees for python code. So, one should ideally look at it and how it is used inside pylint.

注意当然,我可能完全over-analyzing它和一个更简单的'check for white-space or newline'解决方案你们建议就足够了。 :-)

我基于@Jim 的 post 实现了代码。愿它适用于所有情况:

import tokenize
from io import BytesIO

def my_checker(pycode):
    """
    tokenizes python code and yields 
    start, end, strline of any position where 
    a scenario like this happens (missing string seperator):
      [..., "a string" "derp", ...]
    """
    IDLE = 0
    WAITING_STRING = 1
    CHECKING_SEPARATOR = 2

    tokenizer = tokenize.tokenize(BytesIO(pycode.encode('utf-8')).readline)
    state = IDLE

    for toknum, tokval, start, end, strcode  in tokenizer:
        if state == IDLE:
            if toknum == tokenize.OP and tokval == '[':
                state = WAITING_STRING

        elif state == WAITING_STRING:
            if toknum == tokenize.STRING:
                state = CHECKING_SEPARATOR
            elif toknum == tokenize.OP and tokval == [']']:
                state = IDLE

        elif state == CHECKING_SEPARATOR:
            if toknum == tokenize.STRING:
                yield (start, end, strcode)
            elif toknum == tokenize.OP and tokval in ['+', ',']:
                state = WAITING_STRING
            elif toknum == tokenize.OP and tokval == ']':
                state = IDLE

my_code = """
foo = "derp"
def derp(a,x): 
    return str('dingdong'+str(a*x))
[
    "derp"+"FOO22"  , "FOO", "donk" "slurp",0, 0
]

class extreme_logical_class():
    STATIC_BAD_LIST = [0,
        "BLA,",
        "FOO"
        "derp"
    ] 
    def __init__(self):
        self._in_method_check = ["A" "B"]

nested_list = [
    ['DERP','FOO'],
    [0,'hello', 'peter' 'pan'],
    ['this', 'is', ['ultra', 'mega'
        'nested']] 
]
"""

for error in my_checker(my_code):
    print('missing , in list at: line {}@{} to line {}@{}: "{}"'.format(
        error[0][0],error[0][1],error[1][0],error[1][1], error[2].strip()
    ))

结果是:

keksnicoh@localhost ~ % python3 find_bad_lists.py
missing , in list at: line 6@36 to line 6@43: ""derp"+"FOO22"  , "FOO", "donk" "blurp",0 0"
missing , in list at: line 13@8 to line 13@14: ""derp""
missing , in list at: line 16@37 to line 16@40: "self._in_method_check = ["A" "B"]"
missing , in list at: line 20@24 to line 20@29: "[0,'hello', 'peter' 'pan'],"
missing , in list at: line 22@8 to line 22@16: "'nested']]"

在现实生活中我宁愿避免犯这样的错误;有很好的 IDE 之类的 Sublime Text,它允许您使用多光标编辑和格式化列表。如果您习惯了这些概念,这些 "separation" 错误就不会出现在您的代码中。

当然,如果有一个开发团队,可以将这样的工具集成到测试环境中。

此正则表达式将查找出现的问题。您的项目中只有 'search all' 个文件。

\[("[^"]*",[\s]*)*"[^"]*"[\s]*"

https://regex101.com/ 和 NotePad++

中测试