OCLint 实现 Code Review - 给你的代码提提质量

工程代码质量,一个永恒的话题。好的质量的好处不言而喻,团队成员间除了保持统一的风格和较高的自我约束力之外,还需要一些工具来统计分析代码质量问题。

本文就是针对 OC 项目,提出的一个思路和实践步骤的记录,最后形成了一个可以直接用的脚本。如果觉得文章篇幅过长,则直接可以下载脚本

OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...

从官方的解释来看,它通过检查 C、C++、Objective-C 代码来寻找潜在问题,来提高代码质量并减少缺陷的静态代码分析工具

一、OCLint 的下载和安装

有3种方式安装,分别为 Homebrew、源代码编译安装、下载安装包安装。

区别:

如果需要自定义 Lint 规则,则需要下载源码编译安装

如果仅仅是使用自带的规则来 Lint,那么以上3种安装方式都可以

1. Homebrew 安装

在安装前,确保安装了 homebrew。步骤简单快捷

brew tap oclint/formulae 

brew install oclint

2. 安装包安装

进入 OCLint 在 Github 中的地址,选择 Release。选择最新版本的安装包(目前最新版本为:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)

解压下载文件。将文件存放到一个合适的位置。(比如我选择将这些需要的源代码存放到 Document 目录下)

在终端编辑当前环境的配置文件,我使用的是 zsh,所以编辑 .zshrc 文件。(如果使用系统的终端则编辑 .bash_profile 文件)

OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release

export PATH=$OCLint_PATH/bin:$PATH

将配置文件 source 一下。

source .zshrc // 如果你使用系统的终端则执行 soucer .bash_profile

验证是否安装成功。在终端输入oclint --version

3. 源码编译安装

homebrew 安装 CMake 和 Ninja 这2个编译工具

brew install cmake ninja

进入 Github 搜索 OCLint,clone 源码

gc https://github.com/oclint/oclint

或者:git clone https://github.com/oclint/oclint.git

进入 oclint-scripts 目录,执行 ./make 命令。这一步的时间非常长。会下载 oclint-json-compilation-database、oclint-xcodebuild、llvm 源码以及 clang 源码。并进行相关的编译得到 oclint。且必须使用翻墙环境不然会报 timeout。如果你的电脑支持翻墙环境,但是在终端下不支持翻墙,可以查看我的这篇文章

./make

编译结束,进入同级 build 文件夹,该文件夹下的内容即为 oclint。可以看到build/oclint-release。方式2下载的安装包的内容就是该文件夹下的内容。

cd 到根目录,编辑环境文件,比如我 zsh 对应的 .zshrc 文件。编辑下面的内容

  OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release

  export PATH=$OCLint_PATH/bin:$PATH

source 下 .zshrc 文件

source .zshrc // source .bash_profile

进入oclint/build/oclint-release目录执行脚本

cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/

ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib

ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib

这里使用 ln -s,把 lib 中的 clang 和 oclint 链接到 /usr/local/bin 目录下。这样做的目的是为了后面如果编写了自己创建的 lint 规则,不必要每次更新自定义的 rule 库,必须手动复制到 /usr/local/bin 目录下。

验证下 OCLint 是否安装成功。输入 oclint --version


验证下 OCLint 是否安装成功

注意:如果你采用源码编译的时候直接 clone 官方的源码会有问题,编译不过,所以提供了一个可以编译过的版本。分支切换到 llvm-7.0。

4. xcodebuild 的安装

xcode 下载安装好就已经成功安装了

5. xcpretty 的安装

先决条件,你的机器已经安装好了 Ruby gem.

gem install xcpretty

二、 自定义 Rule

OClint 提供了 70+ 项的检查规则,你可以直接去使用。但是某些时候你需要制作自己的检测规则,接下来就说说如何自定义 lint 规则。

1、进入 ~/Document/oclint 目录,执行下面的脚本

oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor

其中,CustomLintRules就是定义的检查规则的名字,ASTVisitor就是你继承的 lint 规则

可以继承的规则有:ASTVisitor、SourceCodeReader、ASTMatcher。

2、执行上面的脚本,会生成下面的文件

Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp

Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp

3、要方便的开发自定义的 lint 规则,则需要生成一个 xcodeproj 项目。切换到项目根目录,也就是 Documents/oclint,执行下面的命令

mkdir Lint-XcodeProject

cd Lint-XcodeProject

touch generate-lint-rules.sh

chmod +x generate-lint-rules.sh

给上面的 generate-lint-rules.sh 里面添加下面的脚本

#! /bin/sh -ecmake -G Xcode \  -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++  \  -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \  -D OCLINT_BUILD_DIR=../build/oclint-core \  -D OCLINT_SOURCE_DIR=../oclint-core \  -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics \  -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \  -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules

4、执行 generate-lint-rules.sh 脚本(./generate-lint-rules.sh)。如果出现下面的 Log 则说明生成 xcodeproj 项目成功

生成 xcodeproj 项目成功
执行 generate-lint-rules.sh 脚本的log

5、打开步骤4生成的项目,看到有很多文件夹,代表 oclint 自带的 lint 规则,我们自定义的 lint 规则在最下面。

看到有很多文件夹,代表 oclint 自带的 lint 规则,我们自定义的 lint 规则在最下面

关于如何自定义 lint 规则的具体还没有深入研究,这里给个例子

<details>

<summary>点击查看示例代码</summary>

#include"oclint/AbstractASTVisitorRule.h"

#include"oclint/RuleSet.h"

using namespace std;

using namespace clang;

using namespace oclint;

#include <iostream>

class MVVMRule : public AbstractASTVisitorRule<MVVMRule>

{

public:

    virtual conststring name() const override {

        return"Property in 'ViewModel' Class interface should be readonly.";

    }

    virtual intpriority() const override {

        return3;

    }

    virtual conststring category() const override {

        return"mvvm";

    }


    virtual unsigned intsupportedLanguages() const override {

        return LANG_OBJC;

    }

#ifdef DOCGEN

    virtual conststd::string since() const override {

        return"0.18.10";

    }

 virtual conststd::string description() const override {

        return"Property in 'ViewModel' Class interface should be readonly.";

    }

    virtual conststd::string example() const override {

        returnR"rst( .. code-block:: cpp @interface FooViewModel : NSObject // This is a "ViewModel" Class. @property (nonatomic, strong) NSObject *bar; // should be readonly. @end )rst";

    }

    virtual conststd::string fileName() const override {

        return"MVVMRule.cpp";

    }

#endif

    virtual voidsetUp() override {}

    virtual voidtearDown() override {}

    /* Visit ObjCImplementationDecl */

    boolVisitObjCImplementationDecl(ObjCImplementationDecl *node) {

        ObjCInterfaceDecl *interface = node->getClassInterface();


        bool isViewModel = interface->getName().endswith("ViewModel");

        if (!isViewModel) {

            returnfalse;

        }

        for (auto property = interface->instprop_begin(),

            propertyEnd = interface->instprop_end(); property != propertyEnd; property++)

        {

            clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;

            if (propertyDecl->getName().startswith("UI")) {

                addViolation(propertyDecl, this);

            }

            auto attrs = propertyDecl->getPropertyAttributes();

            bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) >0;

            if (isReadwrite && isViewModel) {

                addViolation(propertyDecl, this);

            }

        }

        returntrue;

    }

};

staticRuleSetrules(newMVVMRule());

</details>

6、修改自定义规则后就需要编译。

成功后在 Products 目录下会看到对应名称的 CustomLintRulesRule.dylib 文件,就需要复制到 /Documents/oclint/oclint-release/lib/oclint/rules。讲道理,生成新的 lint rule 文件,需要把新的 dylib 文件复制到 /usr/local/lib。因为我们在源代码安装的第4部,设置了 ln -s 链接,所以不需要每次复制到相应文件夹。

但是还是比较麻烦,每次都需要编译新的 lint rule 之后需要将相应的 dylib 文件复制到源代码目录下的 oclint-release/lib/oclint/rules 目录下,本着「可以偷懒绝不动手」的原则,在自定义的 rule 的 target 中,在 Build Phases 选项下 CMake PostBuild Rules 中的脚本下将下面的代码复制进去

cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib

7、规则限定的3个类说明:

RuleBase

 |

 |-AbstractASTRuleBase

 |      |_ AbstractASTVisitorRule

 |            |_AbstractASTMatcherRule

 |

 |-AbstractSourceCodeReaderRule

AbstractSourceCodeReaderRule:eachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类

AbstractASTVisitorRule:可以访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的。在 apply 方法内可以看到代码实现。开发者只需要重载 bool visit* 方法来访问特定类型的节点。其值表明是否继续递归检查

AbstractASTMatcherRule:实现 setUpMatcher 方法,在方法中添加 matcher,当检查发现匹配结果时会调用 callback 方法。然后通过 callback 方法来继续对匹配到的结果进行处理

8、知其所以然

oclint 依赖与源代码的语法抽象树(AST)。开源 clang 是 oclint 获的语法抽象树的依赖工具。你如果想对 AST 有个了解,可以查看这个视频

如果想查看某个文件的 AST 结构,你可以进入该文件的命令行,然后执行下面的脚本

clang -Xclang -ast-dump -fsyntax-only main.m

三、 Homebrew 方式安装的 oclint 如何使用自定义规则

查看 OCLint 安装路径

which oclint

// 输出:/usr/local/bin/oclint

ls -al  /usr/local/bin/oclint

// 输出:本机安装路径

把上面生成的新的 lint rule 下的 dylib 文件复制到步骤1得到的额本机安装路径下

四、 使用 oclint

在命令行中使用

如果项目使用了 Cocopod,则需要指定 -workspace xxx.workspace

每次编译之前需要 clean

实操:

进入项目cd /Workspace/Native/iOS/lianhua

查看项目基本信息xcodebuild -list

//输出

information about project "BridgeLabiPhone":

  Targets:

      BridgeLabiPhone

      lint

  Build Configurations:

      Debug

      Release

  If no build configuration is specified and -scheme is not passed then "Release" is used.

  Schemes:

      BridgeLabiPhone

      lint

编译xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json编译成功后,会在项目的文件夹下出现 compile_commands.json 文件

生成 html 报表oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log报错信息是:oclint: error: one compiler command contains multiple jobs:查找资料,解决方案如下

将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO

在 podfile 中 target 'xx' do 前面添加下面的脚本

post_install do |installer|

  installer.pods_project.targets.each do |target|

      target.build_configurations.each do |config|

          config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"

      end

  end

end

然后继续尝试编译,发现还是报错,但是报错信息改变了,如下


报错信息改变了

看到报错信息是默认的警告数量超过限制,则 lint 失败。事实上 lint 后可以跟参数,所以我们修改脚本如下

oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999

生成了 lint 的结果,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改。

![lint-result-html-report](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-oclint-result-html.png)

如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint

oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html 其他的参数

参数说明

| 名称 | 描述 | 默认阈值 |

| ----------------------- | ---------------------------- | ---- |

| CYCLOMATIC_COMPLEXITY | 方法的循环复杂性(圈负责度) | 10 |

| LONG_CLASS | C类或Objective-C接口,类别,协议和实现的行数 | 1000 |

| LONG_LINE | 一行代码的字符数 | 100 |

| LONG_METHOD | 方法或函数的行数 | 50 |

| LONG_VARIABLE_NAME | 变量名称的字符数 | 20 |

| MAXIMUM_IF_LENGTH |if语句的行数 | 15 |

| MINIMUM_CASES_IN_SWITCH | switch语句中的case数 | 3 |

| NPATH_COMPLEXITY | 方法的NPath复杂性 | 200 |

| NCSS_METHOD | 一个没有注释的方法语句数 | 30 |

| NESTED_BLOCK_DEPTH | 块或复合语句的深度 | 5 |

| SHORT_VARIABLE_NAME | 变量名称的字符数 | 3 |

| TOO_MANY_FIELDS | 类的字段数 | 20 |

| TOO_MANY_METHODS | 类的方法数 | 30 |

| TOO_MANY_PARAMETERS | 方法的参数数 | 10 |

在 Xcode 中使用

在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 Lint


 Xcode 中配置

选择对应的 TARGET -> lint。在 Build Phases 下 Run Script 下写下面的脚本代码

export LC_CTYPE=en_US.UTF-8

cd ${SRCROOT}

xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode

说明,虽然有时候没有编译通过,但是看到如下图的关于代码相关的 warning 则达到目的了。


看到如图关于代码相关的 warning 则达到目的了

lint 结果如下,根据相应的提示信息对代码进行调整。当然这只是一种参考,不一定要采纳 lint 给的提示。


lint 结果如图


五、脚本化

每次都在终端命令行去写 lint 的脚本,效率很低,所以想做成 shell 脚本。需要的同学直接直接拷贝进去,直接在工程的根目录下使用,我这边是一个 Cocopod 工程。拿走拿走别客气

脚本文件多次保存,Confluence提示:Unable to communicate with server. Saving is not possible at the moment. 故放附件文件中,详情请见下面脚本:

脚本化

每次都在终端命令行去写 lint 的脚本,效率很低,所以想做成 shell 脚本。需要的同学直接直接拷贝进去,直接在工程的根目录下使用,我这边是一个 Cocopod 工程。拿走拿走别客气

#!/bin/bash

COLOR_ERR="\033[1;31m"    #出错提示

COLOR_SUCC="\033[0;32m"  #成功提示

COLOR_QS="\033[1;37m"  #问题颜色

COLOR_AW="\033[0;37m"  #答案提示

COLOR_END="\033[1;34m"    #颜色结束符

# 寻找项目的 ProjectName

function searchProjectName () {

# maxdepth 查找文件夹的深度

  find . -maxdepth 1 -name "*.xcodeproj"

}

function oclintForProject () {

    # 预先检测所需的安装包是否存在

    if which xcodebuild 2>/dev/null; then

        echo 'xcodebuild exist'

    else

        echo '🤔️ 连 xcodebuild 都没有安装,玩鸡毛啊? 🤔️'

    fi

    if which oclint 2>/dev/null; then

        echo 'oclint exist'

    else

        echo '😠 完蛋了你,玩 oclint 却不安装吗,你要闹哪样 😠'

        echo '😠 乖乖按照博文:https://github.com/FantasticLBP/knowledge-kit/blob/master/第一部分%20iOS/1.63.md 安装所需环境 😠'

    fi

    if which xcpretty 2>/dev/null; then

        echo 'xcpretty exist'

    else

        gem install xcpretty

    fi

    # 指定编码

    export LANG="zh_CN.UTF-8"

    export LC_COLLATE="zh_CN.UTF-8"

    export LC_CTYPE="zh_CN.UTF-8"

    export LC_MESSAGES="zh_CN.UTF-8"

    export LC_MONETARY="zh_CN.UTF-8"

    export LC_NUMERIC="zh_CN.UTF-8"

    export LC_TIME="zh_CN.UTF-8"

    export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安装位置可以在终端用 which xcpretty找到

    searchFunctionName=`searchProjectName`

    path=${searchFunctionName}

    # 字符串替换函数。//表示全局替换 /表示匹配到的第一个结果替换。

    path=${path//.\//}  # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj

    path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone


    myworkspace=$path".xcworkspace" # workspace名字

    myscheme=$path  # scheme名字

    # 清除上次编译数据

    if [ -d ./derivedData ]; then

        echo -e $COLOR_SUCC'-----清除上次编译数据derivedData-----'$COLOR_SUCC

        rm -rf ./derivedData

    fi

    # xcodebuild clean

    xcodebuild -scheme $myscheme -workspace $myworkspace clean

    # # 生成编译数据

    xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

    if [ -f ./compile_commands.json ]; then

        echo -e $COLOR_SUCC'编译数据生成完毕😄😄😄'$COLOR_SUCC

    else

        echo -e $COLOR_ERR'编译数据生成失败😭😭😭'$COLOR_ERR

        return -1

    fi

    # 生成报表

    oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \

    -rc LONG_LINE=200 \

    -disable-rule ShortVariableName \

    -disable-rule ObjCAssignIvarOutsideAccessors \

    -disable-rule AssignIvarOutsideAccessors \

    -max-priority-1=100000 \

    -max-priority-2=100000 \

    -max-priority-3=100000

    if [ -f ./oclintReport.html ]; then

        rm compile_commands.json

        echo -e $COLOR_SUCC'😄分析完毕😄'$COLOR_SUCC

    else

        echo -e $COLOR_ERR'😢分析失败😢'$COLOR_ERR

        return -1

    fi

    echo -e $COLOR_AW'将为大爷自动打开 lint 的分析结果'$COLOR_AW

    # 用 safari 浏览器打开 oclint 的结果

    open -a "/Applications/Safari.app" oclintReport.html

}

oclintForProject

参考:

OCLint:https://oclint-docs.readthedocs.io/en/stable/

iOS 工程自动化 - OCLint:http://www.cocoachina.com/articles/20181

iOS持续集成(CI)——OCLint静态代码分析:https://cloud.tencent.com/developer/article/1438626

OCLint 自定义规则:https://www.jianshu.com/p/383d4166bec5

OCLint静态代码检测实践:https://juejin.im/post/6844904017424809998

iOS 持续集成系列 - 自动化 Code Review:https://www.jianshu.com/p/c8b3b515ccf3

OCLint静态代码分析:https://chenjiangchuan.gitbooks.io/ios-documentation/content/oclintjing-tai-dai-ma-fen-xi.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容