当前位置: 首页 > java >正文

规范编码策略以及AST的应用的学习

Git Hook实现代码规范

代码规范对于项目维护至关重要。通过Git Hook,我们可以在代码提交前自动执行规范检查,确保代码质量。

Git Hook原理

Git钩子是在Git执行特定操作时触发的脚本。它们位于项目的.git/hooks目录下,可以插入到Git工作流程的各个阶段,如pre-commitcommit-msg等。

通过编写这些钩子脚本,我们可以实现:

  1. 代码风格检查(驼峰命名、括号使用等)
  2. 自动格式化代码
  3. 提交前的单元测试执行

项目实践案例

在我们的APP项目中,主要使用了两个脚本来实现代码规范:

  1. 安装clang-format脚本:确保团队成员都安装了统一的代码格式化工具bash

# 检查并安装clang-format

if ! brew list | grep -q clang-format; then

    log "clang-format is not installed. Proceeding with installation..."

    if ! brew install clang-format; then

        handle_error "Failed to install clang-format"

    fi

    log "clang-format installed successfully."

else

    log "clang-format is already installed. Skipping installation."

fi

  1. pre-commit钩子函数:在提交前自动格式化代码bash

# 使用clang-format格式化代码

find ./Business ./Common -name "*.m" -o -name "*.h" -o -name "*.mm" | xargs clang-format -i

这种方式确保了团队内代码风格的一致性,减少了因格式问题产生的冗余提交。

AST及其应用

AST概述

AST(抽象语法树)是源代码的树形表示,是编译过程中的关键产物。它通过两个阶段生成:

  1. 词法分析:将代码分解为标记(tokens
  2. 语法分析:将标记组织成树状结构

AST作为源代码到汇编代码的桥梁,在编程工具中有广泛应用。

AST应用场景

  1. 代码语法检查
  2. 代码风格检查
  3. 代码格式化
  4. 代码高亮显示
  5. 错误提示
  6. 代码自动补全

AST的理解类比

我们可以将AST理解为语文分析。例如分析"你是猪,"这个句子:

  1. 词法分析:将句子拆分为各个部分javascript

[

  { type: "主语", value: "你" },

  { type: "谓语", value: "是" },

  { type: "宾语", value: "猪" },

  { type: "标点符号", value: "," },

]

  1. 语法分析:生成树结构javascript

{

  type: "语句",

  body: {

    type: "肯定陈述句",

    declarations: [

      {

        type: "声明",

        person: {

          type: "Identifier",

          name: "你",

        },

        name: {

          type: "animal",

          value: "猪",

        },

      },

    ],

  },

};

简易编译器设计

由于ast是编译过程的产物,为了加深对ast的理解,这边试着去设计一个简易的编译器

一个完整的编译器整体执行过程可以分为三个步骤:

  1. Parsing(解析过程):这个过程要经词法分析、语法分析、构建AST(抽象语法树)一系列操作;
  2. Transformation(转化过程):这个过程就是将上一步解析后的内容,按照编译器指定的规则进行处理,形成一个新的表现形式;
  3. Code Generation(代码生成):将上一步处理好的内容转化为新的代码;

举个例子,例如我们的源代码文件生成汇编代码.s文件,就是经过了这个流程

一个例子,将 lisp 的函数调用编译成类似 C 的函数

    LISP 代码: (add 2 (subtract 4 2))

    C    代码  add(2, subtract(4, 2))

    释义: 2 + ( 4 - 2 )

解析

解析过程分为2个步骤:词法分析、语法分析

词法分析是使用tokenizer(分词器)或者lexer(词法分析器),将源码拆分成tokenstokens是一个放置对象的数组,其中的每一个对象都可以看做是一个单元(数字,标签,标点,操作符...)的描述信息。

[

  { type: "paren", value: "(" },

  { type: "name", value: "add" },

  { type: "number", value: "2" },

  { type: "paren", value: "(" },

  { type: "name", value: "subtract" },

  { type: "number", value: "4" },

  { type: "number", value: "2" },

  { type: "paren", value: ")" },

  { type: "paren", value: ")" },

];

语法分析实际上就是将各个tokens整理成相互关联的表达式,实际上就是AST

{

  type: 'Program',

  body: [{

    type: 'CallExpression',

    name: 'add',

    params:

      [{

        type: 'NumberLiteral',

        value: '2',

      },

      {

        type: 'CallExpression',

        name: 'subtract',

        params: [{

          type: 'NumberLiteral',

          value: '4',

        }, {

          type: 'NumberLiteral',

          value: '2',

        }]

      }]

  }]

}

转化

将源代码解析成抽象语法树后,为了生成我们的新代码,就需要改写当前抽象语法树,同时根据新的抽象语法树生成新代码,这个新代码既可以仍然是当前语言,也可以是新语言

Traversal(遍历):顾名思义这个过程就是,遍历这个AST(抽象语法树)的所有节点,这个过程使用深度优先遍历,大概执行顺序如下:

Visitors (访问器)访问器最基本的思想是创建一个访问器”对象,这个对象可以处理不同类型的节点函数,如下所示

    const visitor = {

        NumberLiteral(node,parent){}, // 处理数字类型节点

        CallExpression(node,parent){} // 处理调用语句类型节点

    }

在案例中我们是想将lisp语言转化为C语言,因此需要构建一个新的AST(抽象语法树)。这个创建的过程就需要遍历这个“树”的节点并读取其内容,由此引出 Traversal(遍历) Visitors (访问器)

如果并不需要转换语言,那么就不需要新建,只需要遍历原本AST,同时将对应节点替换为自己所需要的即可

生成阶段

最后就是代码生成阶段了,其实就是将生成的新AST树再转回代码的过程。大部分的代码生成器主要过程是,不断的访问Transformation生成的AST(抽象语法树)或者再结合tokens,按照指定的规则,将上的节点打印拼接最终还原为新的code,自此编译器的执行过程就结束了。

简易编译器原理

生成tokens

这是词法分析的步骤,这个过程需要tokenzier(分词器)函数
思路:
遍历字符串,将每个字符按照规则抽离出来,最终生成tokens数组

简易来做就是遇到

  1. 括号生成新token,
  2. 检测字符是否有空格来进行分隔,但是在token中空格是无意义的
  3. 检测数字或函数name,持续向后匹配直到失败...

function tokenizer (input) {

  let current = 0; //记录当前访问的位置

  let tokens = [] // 最终生成的tokens

  // 循环遍历input

  while (current < input.length) {

    let char = input[current];

    // 如果字符是开括号,我们把一个新的token放到tokens数组里,类型是`paren`

    if (char === '(') {

      tokens.push({

        type: 'paren',

        value: '(',

      });

      current++;

      continue;

    }

    // 闭括号做同样的操作

    if (char === ')') {

      tokens.push({

        type: 'paren',

        value: ')',

      });

      current++;

      continue;

    }

    //空格检查,我们关心空格在分隔字符上是否存在,但是在token中他是无意义的

    let WHITESPACE = /\s/;

    if (WHITESPACE.test(char)) {

      current++;

      continue;

    }

    //接下来检测数字,这里解释下 如果发现是数字我们如 add 22 33 这样

    //我们是不希望被解析为2、2、3、3这样的,我们要遇到数字后继续向后匹配直到匹配失败

    //这样我们就能截取到连续的数字了

    let NUMBERS = /[0-9]/;

    if (NUMBERS.test(char)) {

      let value = '';

      while (NUMBERS.test(char)) {

        value += char;

        char = input[++current];

      }

      tokens.push({ type: 'number', value });

      continue;

    }

    // 接下来检测字符串,这里我们只检测双引号,和上述同理也是截取连续完整的字符串

    if (char === '"') {

      let value = '';

      char = input[++current];

      while (char !== '"') {

        value += char;

        char = input[++current];

      }

      char = input[++current];

      tokens.push({ type: 'string', value });

      continue;

    }

    // 最后一个检测的是name 如add这样,也是一串连续的字符,但是他是没有“”的

    let LETTERS = /[a-z]/i;

    if (LETTERS.test(char)) {

      let value = '';

      while (LETTERS.test(char)) {

        value += char;

        char = input[++current];

      }

      tokens.push({ type: 'name', value });

      continue;

    }

    // 容错处理,如果我们什么都没有匹配到,说明这个token不在我们的解析范围内

    throw new TypeError('I dont know what this character is: ' + char);

  }

  return tokens

}

生成AST

将生成好的token转换为AST

遍历访问生成好的AST,同时根据自己需要进行转化

*   Original AST                     |   Transformed AST

* ----------------------------------------------------------------------------

*   {                                |   {

*     type: 'Program',               |     type: 'Program',

*     body: [{                       |     body: [{

*       type: 'CallExpression',      |       type: 'ExpressionStatement',

*       name: 'add',                 |       expression: {

*       params: [{                   |         type: 'CallExpression',

*         type: 'NumberLiteral',     |         callee: {

*         value: '2'                 |           type: 'Identifier',

*       }, {                         |           name: 'add'

*         type: 'CallExpression',    |         },

*         name: 'subtract',          |         arguments: [{

*         params: [{                 |           type: 'NumberLiteral',

*           type: 'NumberLiteral',   |           value: '2'

*           value: '4'               |         }, {

*         }, {                       |           type: 'CallExpression',

*           type: 'NumberLiteral',   |           callee: {

*           value: '2'               |             type: 'Identifier',

*         }]                         |             name: 'subtract'

*       }]                           |           },

*     }]                             |           arguments: [{

*   }                                |             type: 'NumberLiteral',

*                                    |             value: '4'

* ---------------------------------- |           }, {

*                                    |             type: 'NumberLiteral',

*                                    |             value: '2'

*                                    |           }]

*                                    |         }

*                                    |       }

*                                    |     }]

*                                    |   }

function transformer (ast) {

  // 将要被返回的新的AST

  let newAst = {

    type: 'Program',

    body: [],

  };

  // 这里相当于将在旧的AST上创建一个_content,这个属性就是新AST的body,因为是引用,所以后面可以直接操作就的AST

  ast._context = newAst.body;

  // 用之前创建的访问器来访问这个AST的所有节点

  traverser(ast, {

    // 针对于数字片段的处理

    NumberLiteral: {

      enter (node, parent) {

        // 创建一个新的节点,其实就是创建新AST的节点,这个新节点存在于父节点的body中

        parent._context.push({

          type: 'NumberLiteral',

          value: node.value,

        });

      },

    },

    // 针对于文字片段的处理

    StringLiteral: {

      enter (node, parent) {

        parent._context.push({

          type: 'StringLiteral',

          value: node.value,

        });

      },

    },

    // 对调用语句的处理

    CallExpression: {

      enter (node, parent) {

        // 在新的AST中如果是调用语句,type是`CallExpression`,同时他还有一个`Identifier`,来标识操作

        let expression = {

          type: 'CallExpression',

          callee: {

            type: 'Identifier',

            name: node.value,

          },

          arguments: [],

        };

        // 在原来的节点上再创建一个新的属性,用于存放参数 这样当子节点修改_context时,会同步到expression.arguments中,这里用的是同一个内存地址

        node._context = expression.arguments;

        // 这里需要判断父节点是否是调用语句,如果不是,那么就使用`ExpressionStatement`将`CallExpression`包裹,因为js中顶层的`CallExpression`是有效语句

        if (parent.type !== 'CallExpression') {

          expression = {

            type: 'ExpressionStatement',

            expression: expression,

          };

        }

        parent._context.push(expression);

      },

    }

  });

  return newAst;

}

新代码生成

用新的AST遍历每一个节点,根据对应规则生成最终代码

function codeGenerator(node) {

  // 我们以节点的种类拆解(语法树)

  switch (node.type) {

    // 如果是Progame,那么就是AST的最根部了,他的body中的每一项就是一个分支,我们需要将每一个分支都放入代码生成器中

    case 'Program':

      return node.body.map(codeGenerator)

        .join('\n');

    // 如果是声明语句注意看新的AST结构,那么在声明语句中expression,就是声明的标示,我们以他为参数再次调用codeGenerator

    case 'ExpressionStatement':

      return (

        codeGenerator(node.expression) + ';'

      );

    // 如果是调用语句,我们需要打印出调用者的名字加括号,中间放置参数如生成这样"add(2,2)",

    case 'CallExpression':

      return (

        codeGenerator(node.callee) +  '(' + node.arguments.map(codeGenerator).join(', ') + ')'

      );

     

    // 如果是识别就直接返回值 如: (add 2 2),在新AST中 add就是那个identifier节点

    case 'Identifier':

      return node.name;

    // 如果是数字就直接返回值

    case 'NumberLiteral':

      return node.value;

    // 如果是文本就给值加个双引号

    case 'StringLiteral':

      return '"' + node.value + '"';

    // 容错处理

    default:

      throw new TypeError(node.type);

  }

}

实际应用

使用Babel修改函数名

//源代码

const hello = () => {};

//需要修改为:

const world = () => {};

思路:

  1. 先将源代码转化成AST
  2. 遍历AST上的节点,找到 hello 函数名节点并修改
  3. 将转换过的AST再生成JS代码

实现:
找到name为hello的节点,进行修改

const parser = require("@babel/parser");

const traverse = require("@babel/traverse");

const generator = require("@babel/generator");

// 源代码

const code = `

const hello = () => {};

`;

// 1. 源代码解析成 ast

const ast = parser.parse(code);

// 2. 转换

const visitor = {

  // traverse 会遍历树节点,只要节点的 type 在 visitor 对象中出现,变化调用该方法

  Identifier(path) {

    const { node } = path; //从path中解析出当前 AST 节点

    if (node.name === "hello") {

      node.name = "world"; //找到hello的节点,替换成world

    }

  },

};

traverse.default(ast, visitor);

// 3. 生成

const result = generator.default(ast, {}, code);

console.log(result.code); //const world = () => {};

手写监控系统中的日志上传插件

在监控系统的日志上传过程中,我们需要往每个函数的作用域中添加一行日志执行函数

源代码:

//四种声明函数的方式

function sum(a, b) {

  return a + b;

}

const multiply = function (a, b) {

  return a * b;

};

const minus = (a, b) => a - b;

class Calculator {

  divide(a, b) {

    return a / b;

  }

}

期望转换后的代码:

import loggerLib from "logger"

function sum(a, b) {

  loggerLib()

  return a + b;

}

const multiply = function (a, b) {

  loggerLib()

  return a * b;

};

const minus = (a, b) =>{

  loggerLib()

  return  a - b;

}

class Calculator {

  divide(a, b) {

    loggerLib()

    return a / b;

  }

}

整体思路:

  1. 第一步:先判断源代码中是否引入了logger
  2. 第二步:如果引入了,就找出导入的变量名,后面直接使用该变量名即可
  3. 第三步:如果没有引入我们就在源代码的顶部引用一下
  4. 第四步:在函数中插入引入的函数

前端工程化基石 -- AST(抽象语法树)以及AST的广泛应用🔥本文将从一道小学语文题出发,由浅入深的讲述AST的设计 - 掘金

AST静态分析的工作原理

  1. 代码解析:首先将源代码解析成AST
  2. 遍历AST:使用访问者模式(Visitor Pattern)遍历AST
  3. 规则检查:在遍历过程中编写脚本同时使用各种规则来检查代码问题
  4. 报告生成:收集发现的问题并生成报告

AST静态分析的优势

  1. 全面性:可以分析整个代码库,而不仅仅是执行路径
  2. 早期发现:在编码阶段就能发现潜在问题,而不是等到运行时
  3. 规则可定制:可以根据项目需求定制检查规则
  4. 自动化:可以集成到CI/CD流程中自动检查代码

clang-format代码格式化原理

与完整编译器不同,Clang-Format 执行的是轻量级解析,主要关注代码结构而非完整语义,但同样的,它也会执行词法分析语法分析,都会将代码分割为tokens,并且生成抽象语法树

但clang-format主要关注的是

  1. 计算标记间应放置的空格和换行数量
  2. 基于代码嵌套结构确定缩进量
    以此来优化代码格式,同时提供了一些可选的决策来帮助我们格式化,同时还确保格式化不改变代码含义

clang-format的介绍和使用 - JindouBlog - 博客园

http://www.xdnf.cn/news/2971.html

相关文章:

  • 如何使用 Nacos 对 Python 服务进行服务发现与注册
  • 【Dify系列教程重置精品版】第三章:Dify平台的本地化部署
  • Idea 如何配合 grep console过滤并分析文件
  • 关于浏览器对于HTML实体编码,urlencode,Unicode解析
  • Janus-1B评测
  • vue 优化策略,大白话版本
  • 一页概览:统一数据保护方案
  • Discord多账号注册登录:如何同时管理多个账户?
  • 全球667629个流域90m分辨率数据(流域参数含:面积、长度、宽度、纵横比、坡度和高程等)
  • 借助Spring AI实现智能体代理模式:从理论到实践
  • 案例解析:基于量子计算的分子对接-QDOCK(Quantum Docking)
  • 2025年深圳软件开发公司推荐
  • Laravel+API 接口
  • Mybatis-plus代码生成器的创建使用与详细解释
  • 环境-疲劳载荷综合试验系统
  • AI日报 - 2025年04月30日
  • Weiss Robotics的WPG与WSG系列紧凑型机器人夹爪,精准、灵活、高效
  • LoRA 微调技术详解:参数高效的大模型轻量化适配方案
  • 【嘉立创EDA】如何找到曲线和直线的交点,或找到弧线和直线的交点
  • GESP2024年9月认证C++八级( 第三部分编程题(2)美丽路径)
  • leetcode373.寻找和最小的k对数字
  • 机器人“跨协议对话”秘籍:EtherNet IP转PROFINET网关应用实录
  • mongoose插入文档,字段类型, 字段验证, 删除文档,更新文档,读取文档,查询文档的条件控制 ,字段筛选,数据排序,数据截取
  • [Linux网络_68] 转发 | 路由(Hop by Hop) | IP的分片和组装
  • 住宅代理IP购买指南:保护您的网络行为
  • C++:Lambda表达式
  • YOLO学习笔记 | YOLOv8与卡尔曼滤波实现目标跟踪与预测(附代码)
  • JVM GC垃圾回收算法
  • 面试手撕——快速排序
  • 高翔视觉slam中常见的OpenCV和Eigen的几种数据类型的内存布局及分配方式详解