PHP词法解析源码分析之引号及其内容

在词法解析的代码中,引号里面内容的解析就占了所有代码的一半以上,可见其情况是比较多的,这次就来简单分析一下。由于lex_scan开头的一段整个函数都用得到,所以这里就再贴一下:

int lex_scan(zval *zendlval TSRMLS_DC)
{
//设置当前token的首位置为当前位置
restart:
    SCNG(yy_text) = YYCURSOR;

yymore_restart:
//这段注释定义了各个类型的正则表达式匹配,在词法解析程序(如bison、re2c等)程序将本文件转化为c代码时会用到
/*!re2c
re2c:yyfill:check = 0;
LNUM    [0-9]+
DNUM    ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)
EXPONENT_DNUM    (({LNUM}|{DNUM})[eE][+-]?{LNUM})
HNUM    "0x"[0-9a-fA-F]+
BNUM    "0b"[01]+
LABEL    [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
WHITESPACE [ \n\r\t]+
TABS_AND_SPACES [ \t]*
TOKENS [;:,.\[\]()|^&+-/*=%!~$<>?@]
ANY_CHAR [^]
NEWLINE ("\r"|"\n"|"\r\n")

/* compute yyleng before each rule */
<!*> := yyleng = YYCURSOR - SCNG(yy_text);

1.引号开始的处理

1.1 单引号的处理

可以看到整个的处理流程看似比较复杂,其实上就是单引号内只会转义诸如'\'、'\''、'\n'等转义字符,并不会对诸如\x65、0123之类的十六进制及八进制数进行转义(这里只是举个例子,更多不转义的情况下面会介绍)。同时它还会保证在只有一个单引号的情况下不会报错,而只是返回T_ENCAPSED_AND_WHITESPACE(当然也没有转义)。

<ST_IN_SCRIPTING>b?['] {
    register char *s, *t;
    char *end;
    int bprefix = (yytext[0] != '\'') ? 1 : 0;

    while (1) {
        if (YYCURSOR < YYLIMIT) {
            if (*YYCURSOR == '\'') {
                YYCURSOR++;
                yyleng = YYCURSOR - SCNG(yy_text);

                break;
            } else if (*YYCURSOR++ == '\\' && YYCURSOR < YYLIMIT) {
                YYCURSOR++;
            }
        } else {
            yyleng = YYLIMIT - SCNG(yy_text);

            /* Unclosed single quotes; treat similar to double quotes, but without a separate token
             * for ' (unrecognized by parser), instead of old flex fallback to "Unexpected character..."
             * rule, which continued in ST_IN_SCRIPTING state after the quote */
            return T_ENCAPSED_AND_WHITESPACE;
        }
    }

    ZVAL_STRINGL(zendlval, yytext+bprefix+1, yyleng-bprefix-2, 1);

    /* convert escape sequences */
    s = t = Z_STRVAL_P(zendlval);
    end = s+Z_STRLEN_P(zendlval);
    while (s<end) {
        if (*s=='\\') {
            s++;

            switch(*s) {
                case '\\':
                case '\'':
                    *t++ = *s;
                    Z_STRLEN_P(zendlval)--;
                    break;
                default:
                    *t++ = '\\';
                    *t++ = *s;
                    break;
            }
        } else {
            *t++ = *s;
        }

        if (*s == '\n' || (*s == '\r' && (*(s+1) != '\n'))) {
            CG(zend_lineno)++;
        }
        s++;
    }
    *t = 0;

    if (SCNG(output_filter)) {
        size_t sz = 0;
        s = Z_STRVAL_P(zendlval);
        SCNG(output_filter)((unsigned char **)&Z_STRVAL_P(zendlval), &sz, (unsigned char *)s, (size_t)Z_STRLEN_P(zendlval) TSRMLS_CC);
        Z_STRLEN_P(zendlval) = sz;
        efree(s);
    }
    return T_CONSTANT_ENCAPSED_STRING;
}
1.2 双引号的处理

双引号的处理相对单引号来说容易理解很多。在ST_IN_SCRIPTING状态下遇到双引号开始往后扫描,若是遇到了另一个双引号则直接调用zend_scan_escape_string转义里面的字符串并进行保存然后返回T_CONSTANT_ENCAPSED_STRING表明这是一个常量字符串;若是遇到了诸如'$LABEL','${','{$'这三种情况(字符串中的变量)则停止扫描,保存当前已经扫描的字符并将当前状态改为ST_DOUBLE_QUOTES表明正处于双引号字符串中,然后只返回一个'"'并进入下一轮的扫描;若是遇到了'\'则跳过该字符也进入下一轮处理:

<ST_IN_SCRIPTING>b?["] {
    int bprefix = (yytext[0] != '"') ? 1 : 0;

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '"':
                yyleng = YYCURSOR - SCNG(yy_text);
                zend_scan_escape_string(zendlval, yytext+bprefix+1, yyleng-bprefix-2, '"' TSRMLS_CC);
                return T_CONSTANT_ENCAPSED_STRING;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

    /* Remember how much was scanned to save rescanning */
    SET_DOUBLE_QUOTES_SCANNED_LENGTH(YYCURSOR - SCNG(yy_text) - yyleng);

    YYCURSOR = SCNG(yy_text) + yyleng;

    BEGIN(ST_DOUBLE_QUOTES);
    return '"';
}
1.3 Heredoc和Nowdoc的处理

可以看到heredoc的形式,首先是'<<<'接着可以跟上若干空格或tab字符,接着可以是LABEL,'LABEL'或"LABEL",最后必须要有换行符。在处理里面的内容时首先把开头的空格和tab字符删去,随后若出现了单引号则将当前状态改为ST_NOWDOC,否则则为ST_HEREDOC。然后就是根据计算出的长度来确定是否到达末尾,如果到了末尾则直接将状态改为ST_END_HEREDOC以便下一次扫描结束heredoc的处理:

<ST_IN_SCRIPTING>b?"<<<"{TABS_AND_SPACES}({LABEL}|([']{LABEL}['])|(["]{LABEL}["])){NEWLINE} {
    char *s;
    int bprefix = (yytext[0] != '<') ? 1 : 0;
    zend_heredoc_label *heredoc_label = emalloc(sizeof(zend_heredoc_label));

    CG(zend_lineno)++;
    heredoc_label->length = yyleng-bprefix-3-1-(yytext[yyleng-2]=='\r'?1:0);
    s = yytext+bprefix+3;
    while ((*s == ' ') || (*s == '\t')) {
        s++;
        heredoc_label->length--;
    }

    if (*s == '\'') {
        s++;
        heredoc_label->length -= 2;

        BEGIN(ST_NOWDOC);
    } else {
        if (*s == '"') {
            s++;
            heredoc_label->length -= 2;
        }

        BEGIN(ST_HEREDOC);
    }

    heredoc_label->label = estrndup(s, heredoc_label->length);

    /* Check for ending label on the next line */
    if (heredoc_label->length < YYLIMIT - YYCURSOR && !memcmp(YYCURSOR, s, heredoc_label->length)) {
        YYCTYPE *end = YYCURSOR + heredoc_label->length;

        if (*end == ';') {
            end++;
        }

        if (*end == '\n' || *end == '\r') {
            BEGIN(ST_END_HEREDOC);
        }
    }

    zend_ptr_stack_push(&SCNG(heredoc_label_stack), (void *) heredoc_label);

    return T_START_HEREDOC;
}
1.4 反引号处理

反引号处理比较简单,直接将状态改为ST_BACKQUOTE然后返回'`':

<ST_IN_SCRIPTING>[`] {
    BEGIN(ST_BACKQUOTE);
    return '`';
}

2.引号中内容的处理

前面了解了在ST_IN_SCRIPTING状态下遇到引号类字符的处理过程,后面便是在引号中的各种处理了。引号的状态包括ST_DOUBLE_QUOTES,ST_BACKQUOTE,ST_HEREDOC和ST_NOWDOC,除了ST_NOWDOC之中的内容无法转义(相当于单引号,只能转义\等极少数的字符)之外,其他几个状态中的内容都是可以进行转义的,至于单引号中的内容也是不需要转义的,这里也没有单独的状态来表示进入单引号,而只是T_ENCAPSED_AND_WHITESPACE表示单引号里面的内容。

2.1 引号中的变量

首先是是字符串中有'${'的情况,保存状态之后返回T_DOLLAR_OPEN_CURLY_BRACES表示是'$'与'{'的组合:

<ST_DOUBLE_QUOTES,ST_BACKQUOTE,ST_HEREDOC>"${" {
    yy_push_state(ST_LOOKING_FOR_VARNAME TSRMLS_CC);
    return T_DOLLAR_OPEN_CURLY_BRACES;
}

在下一次扫描时的状态便会改为ST_LOOKING_FOR_VARNAME,将变量名保存并恢复状态为ST_IN_SCRIPTING:

<ST_LOOKING_FOR_VARNAME>{LABEL}[[}] {
    yyless(yyleng - 1);
    zend_copy_value(zendlval, yytext, yyleng);
    zendlval->type = IS_STRING;
    yy_pop_state(TSRMLS_C);
    yy_push_state(ST_IN_SCRIPTING TSRMLS_CC);
    return T_STRING_VARNAME;
}

然后是匹配'$LABEL':

<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    zendlval->type = IS_STRING;
    return T_VARIABLE;
}
2.2 引号中的类、结构体

类和结构体的调用形式是'$LABEL->var',本次扫描只会保存LABEL并不会对后面var进行处理,并将状态变为ST_LOOKING_FOR_PROPERTY等待下一次进行处理,至于寻找属性就是上面3.1的集中情况了:

/* Make sure a label character follows "->", otherwise there is no property
 * and "->" will be taken literally
 */
<ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE>"$"{LABEL}"->"[a-zA-Z_\x7f-\xff] {
    yyless(yyleng - 3);
    yy_push_state(ST_LOOKING_FOR_PROPERTY TSRMLS_CC);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    zendlval->type = IS_STRING;
    return T_VARIABLE;
}
2.3 引号中的数组等

数组就是'$LABEL[var]'这个情况,同样本次扫描也不处理var而只是设置状态为ST_VAR_OFFSET等待下一次处理:

/* A [ always designates a variable offset, regardless of what follows
 */
<ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE>"$"{LABEL}"[" {
    yyless(yyleng - 1);
    yy_push_state(ST_VAR_OFFSET TSRMLS_CC);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    zendlval->type = IS_STRING;
    return T_VARIABLE;
}

下一次进行扫描时便会匹配ST_VAR_OFFSET状态的下值,[]中可以是数字,若是十进制数,超过表示范围则直接保存为字符串否则转化为十进制整数,若是八进制、十六进制和二进制数字,都是直接作为字符串处理:

<ST_VAR_OFFSET>[0]|([1-9][0-9]*) { /* Offset could be treated as a long */
    if (yyleng < MAX_LENGTH_OF_LONG - 1 || (yyleng == MAX_LENGTH_OF_LONG - 1 && strcmp(yytext, long_min_digits) < 0)) {
        ZVAL_LONG(zendlval, strtol(yytext, NULL, 10));
    } else {
        ZVAL_STRINGL(zendlval, yytext, yyleng, 1);
    }
    return T_NUM_STRING;
}

<ST_VAR_OFFSET>{LNUM}|{HNUM}|{BNUM} { /* Offset must be treated as a string */
    ZVAL_STRINGL(zendlval, yytext, yyleng, 1);
    return T_NUM_STRING;
}

同样在[]中还可以包括一些运算符以及其他的符号:

//可以包含TOKENS和大括号双引号和反引号
<ST_VAR_OFFSET>{TOKENS}|[{}"`] {
    /* Only '[' can be valid, but returning other tokens will allow a more explicit parse error */
    return yytext[0];
}
//还可以包括空白字符、反斜杠、单引号以及#,但这些字符否会被略去
<ST_VAR_OFFSET>[ \n\r\t\\'#] {
    /* Invalid rule to return a more explicit parse error with proper line number */
    yyless(0);
    yy_pop_state(TSRMLS_C);
    return T_ENCAPSED_AND_WHITESPACE;
}

还有就是不带'$'的LABEL,直接作为字符串处理:

<ST_IN_SCRIPTING,ST_VAR_OFFSET>{LABEL} {
    zend_copy_value(zendlval, yytext, yyleng);
    zendlval->type = IS_STRING;
    return T_STRING;
}

3.引号中ANY_CHAR的处理

当处于ST_DOUBLE_QUOTES、ST_BACKQUOTE等引号状态时,如果没有匹配到第二节的情况便会到匹配ANY_CHAR的流程中。

3.1 双引号ST_DOUBLE_QUOTES

首先是判断有没有已经扫描过的字符,有的话直接保存并重新设置扫描的长度然后返回进行下一次的扫描,如果已扫描字符为空则后面的流程和第一次扫描到双引号时的差不多,都是碰到'\\'跳过碰到'"''\$''{\$'等就停止本次扫描,保存当前扫描过的字符然后返回并进行下一次的扫描,总体流程比较简单:

<ST_DOUBLE_QUOTES>{ANY_CHAR} {
    if (GET_DOUBLE_QUOTES_SCANNED_LENGTH()) {
        YYCURSOR += GET_DOUBLE_QUOTES_SCANNED_LENGTH() - 1;
        SET_DOUBLE_QUOTES_SCANNED_LENGTH(0);

        goto double_quotes_scan_done;
    }

    if (YYCURSOR > YYLIMIT) {
        return 0;
    }
    if (yytext[0] == '\\' && YYCURSOR < YYLIMIT) {
        YYCURSOR++;
    }

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '"':
                break;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

double_quotes_scan_done:
    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, '"' TSRMLS_CC);
    return T_ENCAPSED_AND_WHITESPACE;
}
3.2 反引号ST_BACKQUOTE

可以看到除了没有最开始判断已经扫描的长度之外其余的的操作都和双引号几乎相同,所以在词法解析这一层面双引号和反引号可以说非常相似:

<ST_BACKQUOTE>{ANY_CHAR} {
    if (YYCURSOR > YYLIMIT) {
        return 0;
    }
    if (yytext[0] == '\\' && YYCURSOR < YYLIMIT) {
        YYCURSOR++;
    }

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '`':
                break;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, '`' TSRMLS_CC);
    return T_ENCAPSED_AND_WHITESPACE;
}
3.3 ST_HEREDOC

heredoc的其实和双引号差不多,所以在解析的过程中只是在双引号的基础上增加了对heredoc结尾的判断和处理:

<ST_HEREDOC>{ANY_CHAR} {
    int newline = 0;

    zend_heredoc_label *heredoc_label = zend_ptr_stack_top(&SCNG(heredoc_label_stack));

    if (YYCURSOR > YYLIMIT) {
        return 0;
    }

    YYCURSOR--;

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '\r':
                if (*YYCURSOR == '\n') {
                    YYCURSOR++;
                }
                /* fall through */
            case '\n':
                //这个分支判断是否到了heredoc的结尾
                /* Check for ending label on the next line */
                if (IS_LABEL_START(*YYCURSOR) && heredoc_label->length < YYLIMIT - YYCURSOR && !memcmp(YYCURSOR, heredoc_label->label, heredoc_label->length)) {
                    YYCTYPE *end = YYCURSOR + heredoc_label->length;

                    if (*end == ';') {
                        end++;
                    }

                    if (*end == '\n' || *end == '\r') {
                        /* newline before label will be subtracted from returned text, but
                         * yyleng/yytext will include it, for zend_highlight/strip, tokenizer, etc. */
                        if (YYCURSOR[-2] == '\r' && YYCURSOR[-1] == '\n') {
                            newline = 2; /* Windows newline */
                        } else {
                            newline = 1;
                        }

                        CG(increment_lineno) = 1; /* For newline before label */
                        BEGIN(ST_END_HEREDOC);

                        goto heredoc_scan_done;
                    }
                }
                continue;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT && *YYCURSOR != '\n' && *YYCURSOR != '\r') {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

heredoc_scan_done:
    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng - newline, 0 TSRMLS_CC);
    return T_ENCAPSED_AND_WHITESPACE;
}

之前说过nowdoc相对heredoc就相当于单引号相对于双引号,所以在heredoc的基础上删除对'\$'、'{'等的处理就是nowdoc的处理流程了。这里就不贴出对应的处理代码了。

4.引号结束的处理

因为heredoc和nowdoc在处理ANY_CHAR的时候就包括对末尾的处理,并且单引号字符串的处理都在遇到单引号时就搞定了,所以这里只有双引号和反引号结束的处理了。

4.1 双引号结束

将状态设置回ST_IN_SCRIPTING并返回'"'

<ST_DOUBLE_QUOTES>["] {
    BEGIN(ST_IN_SCRIPTING);
    return '"';
}
4.2 反引号的处理

与双引号类似:

<ST_BACKQUOTE>[`] {
    BEGIN(ST_IN_SCRIPTING);
    return '`';
}

在词法分析阶段对引号及其里面内容的处理差不多就是这些了,从匹配引号开头到引号中的变量,再到引号中的ANY_CHAR,最后则是引号结束的处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值