如何自定义KtLint Rules

背景:我司产品序列化类的成员变量需要序列化时没赋默认值,反序列化时可能会出现空指针崩溃,因此在初始化时需要给需要序列化的成员变量赋默认值,因此需要自定义KtLint Rules去在打包前扫描代码,发现没有给序列化成员变量赋默认值的代码报错提示开发人员处理。

1. KtLint是什么

KtLint就是Kotlin版的Lint检查Kotlin代码的规范。
通过如下代码安装,可以在命令行打印某个文件的抽象语法树等功能,后面我们需要用到。

curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.36.0/ktlint &&
  chmod a+x ktlint &&
  sudo mv ktlint /usr/local/bin/

2. 怎么引入自己写的KtLint

刚开始想使用不引入插件的方式去使用自定义Rules,但是发现Gradle找不到ktlint方法,搜索了一番,说有可能是自定义了一个ktlint任务导致重名找不到,尝试重命名ktlint任务为ktlint2任务,还是找不到这个方法,再猜想可能是Gradle版本过低,升级了一下发现也是没有,Google搜索了一下也没说哪个Plugin提供ktlint方法,因此就使用了Pinterest推荐的使用第三方插件实现。

使用pinterrest推荐的第三方Gradle插件,有两个推荐,我用的是jlleitschuh/ktlint-gradle
with Gradle
(with a plugin - Recommended)

Gradle plugins (in order of appearance):

  • jlleitschuh/ktlint-gradle
  • Gradle plugin that automatically creates check and format tasks for project Kotlin sources, supports different kotlin plugins and Gradle build caching.

在root build.gradle加入配置

buildscript {
    dependencies {
        classpath "org.jlleitschuh.gradle:ktlint-gradle:9.1.1"
    }

    repositories {
       // org.jlleitschuh.gradle:ktlint-gradle:9.1.1的仓库
       maven {
            url "https://plugins.gradle.org/m2/"
       }
    }
}

使用这个插件需要把Gradle插件升级到 >= 3.5.3,分发版本 >= 5.4.1

// root build.gradle
classpath 'com.android.tools.build:gradle:3.5.3'

// gradle-wapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

在根build.gradle的allProject下加上如下配置

// 在allProject下,表明所有模块都需要加载这个ktlint配置
ktlint {
        version = "0.36.0" //pinterrest.ktlint标准插件版本
        debug = true
        verbose = true
        android = true // 是否为android平台
        outputToConsole = true
        outputColorName = "RED"
        ignoreFailures = true
        enableExperimentalRules = false // 是否开启实验性规则
    // additionalEditorconfigFile = file("/some/additional/.editorconfig")
        // 禁用规则,ktlint报错时会附带错误规则,不需要的加在这里就可以
        disabledRules = ["import-ordering", "max-line-length", "parameter-list-wrapping"]
        reporters {
            reporter "plain"
            reporter "checkstyle"
      // 这里可以自定义reporter的位置
//        customReporters {
//            "csv" {
//                fileExtension = "csv"
//                dependency = project(":project-reporters:csv-reporter")
//            }
//            "yaml" {
//                fileExtension = "yml"
//                dependency = "com.example:ktlint-yaml-reporter:1.0.0"
//            }
//        }
        }
        kotlinScriptAdditionalPaths {
            include fileTree("scripts/")
        }
        filter {
            exclude("**/generated/**")
            include("**/kotlin/**")
            include("**/java/**")
        }
    }

当上面的配置加完以后,可以新建一个custom-ktlint模块来编写自己的规则,然后通过如下代码在其他模块引用自己写的KtLint规则,ktlintRuleset就是jlleitschuh/ktlint-gradle提供的方法。

ktlintRuleset project(":custom-ktlint")

3. 自己定义的规则怎么写

需要自己编写KtLint检测规则就需要用到PSI,PSI是Program Structure Interface(编程结构接口)的缩写
首先新建一个module,命名为custom-ktlintsrc/main/java/packageName下新建一个规则代码继承com.pinterest.ktlint.core.Rule,需要实现父类的visit接口:

override fun visit(
    node: ASTNode,
    autoCorrect: Boolean,
    emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
)

新建一个继承com.pinterest.ktlint.core.RuleSetProvider的类,这个是为了提供自定义规则的提供器,插件会扫描到Provider提供的规则代码,执行其中的visit方法,这样我们就可以使用我们自己的自定义规则。如下所示:

package com.vega.ktlint

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class VegaRuleSetProvider : RuleSetProvider {
    override fun get(): RuleSet = RuleSet(
        "vega-ktlint-rules",
    // 可以提供多个规则,现在只加了一个需要序列化的成员变量没有赋默认值的规则。
        KtSerializationDefaultValueRule()
    )
}

src/main/resources下,新建一个META-INF/services/com.pinterest.ktlint.core.RuleSetProvider纯文本文件,在里面把我们的Provider的全限定名写在里面,插件就可以扫描到我们的Provider了,文件内容如下:

com.vega.ktlint.VegaRuleSetProvider

4. PSI是什么

PSI是Program Structure Interface(编程结构接口)的缩写,其实是JetBrain自己内部定义的一种结构,用于存储AST抽象语法树

图4-1:add函数与代表它的抽象语法树
图4-2:add函数对应的PSI描述
图4-3:KtBlockExpression的子树

可以通过命令获取一个文件的PSI描述

ktlint libveapi/src/main/java/com/vega/ve/api/data.kt --print-ast >> ~/Desktop/data_kt_ast.txt

5. 一个具体描述PSI的例子

  • PSI会将一个类文件所有的描述事无巨细地记录下来,包括里面的空格有多少个,注释等。
  • 对应的我们就可以操作PSI元素,查找代码写否符合我们的规范。
  • 每一行前面波浪号如果是对齐的,说明对应的类型在抽象语法树的同一层级。

FeedItem这个类的PSI描述:

~.psi.KtClass (CLASS)
 87:     ~.psi.KtDeclarationModifierList (MODIFIER_LIST)                //6treePrev,找到了类的修饰符列表,
 87:       ~.psi.KtAnnotationEntry (ANNOTATION_ENTRY)           //Kotlin Serialization是通过注解来描述这个类可以序列化
 87:         ~.c.i.p.impl.source.tree.LeafPsiElement (AT) "@"       //注解需要通过修饰符列表来找到,看第五节代码是怎么获取修饰符列表节点的
 87:         ~.psi.KtConstructorCalleeExpression (CONSTRUCTOR_CALLEE)
 87:           ~.psi.KtTypeReference (TYPE_REFERENCE)
 87:             ~.psi.KtUserType (USER_TYPE)
 87:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 87:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Keep"
 87:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n"
 88:       ~.c.i.p.impl.source.tree.LeafPsiElement (DATA_KEYWORD) "data"
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //5treePrev
 88:     ~.c.i.p.impl.source.tree.LeafPsiElement (CLASS_KEYWORD) "class"                //4treePrev
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //3treePrev
 88:     ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Author"                  //2treePrev
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //1treePrev
 88:     ~.psi.KtPrimaryConstructor (PRIMARY_CONSTRUCTOR)               //关键,找的是构造函数
 88:       ~.c.i.p.impl.source.tree.LeafPsiElement (CONSTRUCTOR_KEYWORD) "constructor"
 88:       ~.psi.KtParameterList (VALUE_PARAMETER_LIST)
 88:         ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("             //构造函数的左括号
 88:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 89:         ~.psi.KtParameter (VALUE_PARAMETER)                    //参数
 89:           ~.psi.KtDeclarationModifierList (MODIFIER_LIST)
 89:             ~.psi.KtAnnotationEntry (ANNOTATION_ENTRY)
 89:               ~.c.i.p.impl.source.tree.LeafPsiElement (AT) "@"
 89:               ~.psi.KtConstructorCalleeExpression (CONSTRUCTOR_CALLEE)
 89:                 ~.psi.KtTypeReference (TYPE_REFERENCE)
 89:                   ~.psi.KtUserType (USER_TYPE)
 89:                     ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 89:                       ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "SerializedName" // 注意看这里的层级关系,代码中会获取成员变量是否带有SerializedName注解
 89:               ~.psi.KtValueArgumentList (VALUE_ARGUMENT_LIST)
 89:                 ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
 89:                 ~.psi.KtValueArgument (VALUE_ARGUMENT)
 89:                   ~.psi.KtStringTemplateExpression (STRING_TEMPLATE)
 89:                     ~.c.i.p.impl.source.tree.LeafPsiElement (OPEN_QUOTE) """
 89:                     ~.psi.KtLiteralStringTemplateEntry (LITERAL_STRING_TEMPLATE_ENTRY)
 89:                       ~.c.i.p.impl.source.tree.LeafPsiElement (REGULAR_STRING_PART) "uid"
 89:                     ~.c.i.p.impl.source.tree.LeafPsiElement (CLOSING_QUOTE) """
 89:                 ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
 89:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "uid"
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (COLON) ":"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.psi.KtTypeReference (TYPE_REFERENCE)
 90:             ~.psi.KtUserType (USER_TYPE)
 90:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 90:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Long"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 90:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "INVALID_ID"
 90:         ~.c.i.p.impl.source.tree.LeafPsiElement (COMMA) ","
 90:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 91:         ~.psi.KtParameter (VALUE_PARAMETER)                    // 下一个参数
 91:           
 93:         ...中间省略一节VALUE_PARAMETER的声明,和上面的一样...
 97:         
 99:         ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"     //这里是构造函数的右括号,构造函数PSI的叶子节点,也是结束点了
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "       //空格字符,1次treeNext
 99:     ~.c.i.p.impl.source.tree.LeafPsiElement (COLON) ":"            //空格前面是冒号,Kotlin用冒号代表继承实现,2次treeNext
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "       //空格字符,3次treeNext
 99:     ~.psi.KtSuperTypeList (SUPER_TYPE_LIST)                //类的超类列表(父类,接口等),4次treeNext
 99:       ~.psi.KtSuperTypeEntry (SUPER_TYPE_ENTRY)                //看第五节代码是怎么获取超类列表节点的
 99:         ~.psi.KtTypeReference (TYPE_REFERENCE)
 99:           ~.psi.KtUserType (USER_TYPE)
 99:             ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 99:               ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Serializable"
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 99:     ~.psi.KtClassBody (CLASS_BODY)             //类的Body,其实我们还有一种情况是序列化的成员变量声明在类Body里还没处理
 99:       ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{"
 99:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
100:       ~.psi.KtObjectDeclaration (OBJECT_DECLARATION)
100:         ~.psi.KtDeclarationModifierList (MODIFIER_LIST)
100:           ~.c.i.p.impl.source.tree.LeafPsiElement (COMPANION_KEYWORD) "companion"
100:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
100:         ~.c.i.p.impl.source.tree.LeafPsiElement (OBJECT_KEYWORD) "object"
100:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
100:         ~.psi.KtClassBody (CLASS_BODY)
100:           ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{"
100:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n        "
101:           ~.psi.KtProperty (PROPERTY)
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val"
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "EmptyAuthor"
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.psi.KtCallExpression (CALL_EXPRESSION)
101:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Author"
101:               ~.psi.KtValueArgumentList (VALUE_ARGUMENT_LIST)
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
101:                 ~.psi.KtValueArgument (VALUE_ARGUMENT)
101:                   ~.psi.KtValueArgumentName (VALUE_ARGUMENT_NAME)
101:                     ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
101:                       ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "isAuthor"
101:                   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:                   ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
101:                   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:                   ~.psi.KtConstantExpression (BOOLEAN_CONSTANT)
101:                     ~.c.i.p.impl.source.tree.LeafPsiElement (FALSE_KEYWORD) "false"
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
101:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
102:           ~.c.i.p.impl.source.tree.LeafPsiElement (RBRACE) "}"
102:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n\n    "
104:       ~.psi.KtNamedFunction (FUN)
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (FUN_KEYWORD) "fun"
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "isIllegal"
104:         ~.psi.KtParameterList (VALUE_PARAMETER_LIST)
104:           ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
104:           ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.psi.KtBinaryExpression (BINARY_EXPRESSION)
104:           ~.psi.KtThisExpression (THIS_EXPRESSION)
104:             ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
104:               ~.c.i.p.impl.source.tree.LeafPsiElement (THIS_KEYWORD) "this"
104:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:           ~.psi.KtOperationReferenceExpression (OPERATION_REFERENCE)
104:             ~.c.i.p.impl.source.tree.LeafPsiElement (EQEQ) "=="
104:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:           ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
104:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "EmptyAuthor"
104:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n"
105:       ~.c.i.p.impl.source.tree.LeafPsiElement (RBRACE) "}"
105:   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n\n"

6. 序列化数据类成员变量必须有默认值的规则

package com.vega.ktlint

import com.pinterest.ktlint.core.Rule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.org.jline.utils.Log
import org.jetbrains.kotlin.psi.KtModifierList
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
import org.jetbrains.kotlin.psi.KtSuperTypeList
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class KtSerializationDefaultValueRule : Rule("kt-serialization-default-value") {
    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == KtStubElementTypes.PRIMARY_CONSTRUCTOR) {
            var hasSerializableAnnotation = false

            // 6MODIFIER_LIST.5WHITESPACE.4CLASS_KEYWORD.3WHITESPACE.2IDENTIFIER.1WHITESPACE.PRIMARY_CONSTRUCTOR
            val modifierListNode = node.treePrev?.treePrev?.treePrev?.treePrev?.treePrev?.treePrev
            //PRIMARY_CONSTRUCTOR.1WHITESPACE.2COLON.3WHITESPACE.4SUPER_TYPE_LIST
            val superTypeListNode = node.treeNext?.treeNext?.treeNext?.treeNext
            if (modifierListNode?.elementType == KtStubElementTypes.MODIFIER_LIST) {
                val modifierListPsi = modifierListNode?.psi as KtModifierList
                val annotationEntries = modifierListPsi.annotationEntries

                // 找出是否为序列化类,有@Serializable注解 或者继承Serializable的都是
                if (annotationEntries.isNotEmpty()) {
                    for (annotationEntry in annotationEntries) {
                        if (annotationEntry.text == "@Serializable") {
                            hasSerializableAnnotation = true
                            break
                        }
                    }
                }
            }

            // 找出继承Serializable的类,并且字段有SerializedName修饰的字段,如果没有默认值,那么就是GSON序列化类没有赋值,错误
            if (hasSerializableAnnotation.not() && superTypeListNode?.elementType == KtStubElementTypes.SUPER_TYPE_LIST) {
                val superTypeListPsi = superTypeListNode?.psi as? KtSuperTypeList
                if (superTypeListPsi != null) {
                    for (superTypeEntry in superTypeListPsi.entries) {
                        val identifier =
         superTypeEntry.typeAsUserType?.referenceExpression?.getIdentifier()
                                ?.text
                        if (identifier == "Serializable") {
                            hasSerializableAnnotation = true
                        }
                    }
                }
            }

            if (hasSerializableAnnotation) {
                val ktPrimaryConstructor = node.psi as? KtPrimaryConstructor
                val valueParamList =
                    ktPrimaryConstructor?.valueParameterList?.parameters
                if (valueParamList != null) {
                    for (param in valueParamList) {
                        val fieldAnnotationEntries = param.modifierList?.annotationEntries
                        if (fieldAnnotationEntries?.isNotEmpty() == true) {
                            var hasSerialNameAnnotation = false

                            for (fieldAnnoEntry in fieldAnnotationEntries) {
                                val fieldAnnoTextIdentifier =
                                    fieldAnnoEntry.calleeExpression?.typeReference?.text
                                // Kotlin Serialization
                                if (fieldAnnoTextIdentifier == "SerialName" ||
                                    // GSON Serialization
                                    fieldAnnoTextIdentifier == "SerializedName"
                                ) {
                                    hasSerialNameAnnotation = true
                                    break
                                }
                            }

                            if (hasSerialNameAnnotation && param.equalsToken == null) {
                                emit(
                                    param.startOffset,
                                    "constructor value param must has default value",
                                    false
                                )
                            }
                        } else {
                            Log.info("fieldModifierList.isEmpty")
                        }
                    }
                } else {
                    // Log.info("2visit valueParamList == null, className=${node.treePrev?.text} do not scan")
                }
            }
        }
    }
}

7. 扫描结果

我们在libfeedtemplate/draft模块下加了ktlintRuleset project(":custom-ktlint"),在终端下执行如下Gradle任务:

 ./gradlew ktlintCheck

得到结果如下,可以看到确实扫描出了FeedCategoryItem的需要序列化的参数没有带默认值。

检测结果

8. CI集成(TODO)

把原有的ktlint任务改成ktlintCheck任务,report的位置也需要对应改下:

"plain" report written to /Users/laizuling/Develop/vega-backup/libfeed/build/reports/ktlint/ktlintMainSourceSetCheck.txt
"checkstyle" report written to /Users/laizuling/Develop/vega-backup/libfeed/build/reports/ktlint/ktlintMainSourceSetCheck.xml

9. 参考链接

Make Your Code Clean with Ktlint
Pinterest KtLint GitHub
IntelliJ Platform SDK

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

推荐阅读更多精彩内容