HamronyOS开发5.0【埋点】总结

1. 解析源文件结构

脚本埋点,需要首先要能正确的解析源文件结构

1.1 注释解析

HarmonyOS UI层文件均遵循的是TypeScript语言注释规则

1.1.1 多行注释

/ * 开头,以 * / 结尾,两个标识都十分容易通过程序辨识

1.1.2 单行注释

// 开头

1.1.3 数据结构

注释解析完成后,仅仅需要将其位置存储下来。因此可定义一个基础代码通用数据结构

/**
 * 代码起始位置
 */
class CodeIndex{
  public startIndex: number = 0
  public endIndex: number = 0

  constructor(s: number, e: number) {
    //起始位置
    this.startIndex = s
    //结束位置
    this.endIndex = e
  }
}

由于整个解析过程都是在“hvigorfile.ts”文件中完成的,这里直接定义两个注释变量,如下:

//多行注释
let mulCodeIndex: CodeIndex[] = []
//单行注释
let singleCodeIndex: CodeIndex[] = []

对于这两个变量,可以只定义一个

对于多行注释和单行注释,在解析的先后顺序上,没有先后,因为两者的格式都可以相互被包含在对方内容中。

我是优先解析多行注释,原因是它解析起来相对比较简单

1.2 字符串解析

在TypeScript中,源码标识字符串的标识总共有三种

  1. "
  2. `

例如

let a: string = 'demo'
let b: string = "demo"
let c: string = `demo`

解析字符串的首要准备条件,必须确定字符串是以哪种标识表示的,所以在遍历过程中,遍历下标每移动一次,需要把三个标识全部查找一遍,然后由小到大排序,取最小下标,然后截取字符串。

同时基于注释范围,遍历过程中,需要忽略注释内的字符串。

1.3 类结构解析

每个以".ets"和".ts"结尾的文件,在其内部,可能出现多个类,例如

@Component
struct TestOne{
  build(){
  
  }
}

class TestTwo{

}

@Entry
@Component
struct TestThree{
  build(){
  
  }
}

通过观察上边的Test.ets文件可以发现,如果我们要在Test.ets中对@Component组件进行生命周期代码插入,可能就比较麻烦,因为有两个@Component

为了方便后续代码插入,还需要对文件中的类进行整体的结构化,结构化之前,需要清楚一个文件中,到底会出现哪些样式的类?

根据HarmonyOS SDK common.d.ts文件中的 “ClassDecorator” 类型可知,ArkTS中会出现的类有4种,

  1. @Entry
  2. @Component
  3. @Observed
  4. @CustomDialog

这4种装饰器修饰的类,其对应的关键词为"struct"

根据TypeScript语言规范可知,类的关键词为"class"

结构化文件,这里可以根据类的类型定义一个Map,从而达到能对一个文件中的多个类进行精准分割

let programClassMap: Map<string, CodeIndex[]> = new Map<string, CodeIndex[]>()

结构化之后,如果要在如上Test.ets文件中的@Component装饰类中进行变量定义,就可以使用如下形式进行每个类的代码遍历

let codeIndex: CodeIndex[] = programClassMap.get('@Component')

//开始遍历
......

至此3步,已基本可以确定整个源码结构,并且可以通过脚本对源码进行插入动作

2. 文件过滤

2.1 服务卡片

关于服务卡片的ets文件, 暂时无法引用自定义的埋点文件,因此必须对其过滤

根据HarmonyOS服务卡片开发指南可知,所有的服务卡片的UI配置均在 form_config.json 文件中

2.2 无@Component装饰器

由于这里做的埋点目标是页面生命周期和点击事件,所以在一个文件内,如果没有@Component装饰器,则忽略对文件进行后续操作

3. 埋点信息

3.1 页面生命周期

@Entry 和 @Component对应生命周期函数是不一样的,因此在代码插入时,需要分别处理

注意:一个 struct 类,可能同时被@Entry 和 @Component修饰,所以在插入代码中,需要依次判断

if(funName == 'onPageShow' || funName == 'onPageHide' || funName == 'onBackPress') {
    //@Entry修饰的 struct 类
    ......
}


if(funName == 'aboutToAppear' || funName == 'aboutToDisappear') {
   //@Component修饰的 struct 类
   ......
}

3.1.1 基础信息

这篇文章,针对生命周期,定义了5个基础数据

  1. 行为序列编号(即 每次从主模块主入口进入应用时的标识)
  2. 页面所属模块
  3. 页面所在的容器
  4. 页面所在的文件
  5. 生命周期名称
01-21 21:12:21.036 14494-16064/? I 0FEFE/JsApp: 生命周期埋点:1705842740475:entry:EntryAbility:entry/src/main/ets/pages/Index.ets:aboutToAppear

3.2 点击事件

onClick是HarmonyOS的一个通用API,这篇文章中的代码插入目标就是它。

onClick的参数是一个无返回值的方法

本篇文章仅针对箭头函数做代码插入

3.2.1 常见写法

//箭头函数
Text().onClick(() => console.log('箭头函数,无花括号'))

Text().onClick(() => {
 console.log('箭头函数,带花括号')
})


clickTest(){
  console.log('测试点击')
}

Text().onClick(this.clickTest)

3.2.2 基础信息

这篇文章中,针对点击事件,定义了3个基础数据

  1. 行为序列编号(即 每次从主模块主入口进入应用时的标识)
  2. 事件被哪个文件消耗
  3. 事件被哪个onClick消耗
public static click(fileName: string, componentName: string){
  console.log('点击埋点:' + this.startBootTime + ' : ' + fileName + ' -> ' + componentName);
}

3.3 Entry模块入口

应用内的所有埋点信息,应该带有使用次数的概念,对于这个次数的定义,从程序来定义,即经过应用主入口算作一次。对应的文件,可以从entry模块中的module.json5文件中找到

{
  "module": {
    "name": "entry",
    "type": "entry",
    "srcEntry": "./ets/AppAbilityStage.ets", //这个为应用主入口文件
   ......
   }
  ......
}

所以,脚本执行开始时,需要在主入口文件中的 onCreate()方法中,插入一个唯一标识,我的脚本采用时间戳

private static startBootTime: number = 0

4. 脚本说明

4.1 API接口

  • copyConfigFile
    复制埋点文件至模块ets文件夹下
  • initAppEntry
    Entry模块主入口初始化埋点行为序号
  • extractCodeIndex
    提取注释,提取字符串,结构化文件中的类
  • insertLifecycleFunction
    插入生命周期埋点
  • insertContentToOnClick
    onClick函数增加埋点

4.2 编译之后的工程源文件效果

1

4.3 DevEco Studio 中日志效果

2

4.4 脚本源文件

  1. 脚本源码需直接 hvigorfile.ts 文件中
  2. 埋点源文件,需放在工程根目录下的Project中

PageLifecycle.ets

import common from '@ohos.app.ability.common';

export default class PageLifecycle{
  private static startBootTime: number = 0

  public static boot() {
    this.startBootTime = new Date().getTime()
  }

  public static record(uiContext: common.UIAbilityContext, filePath: string,  funName: string){
    console.log('生命周期埋点:' + this.startBootTime + ':' + uiContext.abilityInfo.moduleName + ':'+
    uiContext.abilityInfo.name + ':' + filePath + ':' + funName)
  }

  public static click(filePath: string, componentName: string){
    console.log('点击埋点:' + this.startBootTime + ':' + filePath + ':' + componentName);
  }

}

植入在hvigorfile.ts 文件中的脚本源码

console.log('开始执行')

import * as fs from 'fs';
import * as path from 'path';

let mulCodeIndex: CodeIndex[] = []
let singleCodeIndex: CodeIndex[] = []
let stringIndex: CodeIndex[] = []

const INSERT_FUNCTION: string[] = [
  'aboutToAppear',
  'aboutToDisappear',
  'onPageShow',
  'onPageHide',
  'onBackPress'
]

const PAGELIFECYCLE_NAME = 'PageLifecycle.ets'

//开始复制埋点文件
copyConfigFile(process.cwd() + `/Project/${PAGELIFECYCLE_NAME}`, __dirname + `/src/main/ets/${PAGELIFECYCLE_NAME}`)

setTimeout(()=>{
  //初始化应用入口
  initAppEntry(__dirname + `/src/main/ets/${PAGELIFECYCLE_NAME}`, PAGELIFECYCLE_NAME)

  //遍历所有带@Entry装饰器的自定义组件
  findAllFiles(__dirname + '/src/main/ets/', __dirname + '/src/main/ets/', PAGELIFECYCLE_NAME);
}, 10)

/**
 * 复制埋点入口文件至目标地址
 *
 * @param originFile
 * @param targetFilePath
 */
function copyConfigFile(originFile: string, targetFilePath: string){
  let config = fs.readFileSync(originFile,'utf8');
  console.log(config)

  fs.writeFileSync(targetFilePath, config)
}

/**
 * 文件遍历方法
 * @param filePath 需要遍历的文件路径
 */
function findAllFiles(codeRootPath: string, filePath: string, configFileName: string) {

  // 根据文件路径读取文件,返回一个文件列表
  fs.readdir(filePath, (err, files) => {
    if (err) {
      console.error(err);
      return;
    }

    // 遍历读取到的文件列表
    files.forEach(filename => {

      // path.join得到当前文件的绝对路径
      const filepath: string = path.join(filePath, filename);

      // 根据文件路径获取文件信息
      fs.stat(filepath, (error, stats) => {

        if (error) {
          console.warn('获取文件stats失败');
          return;
        }

        const isFile = stats.isFile();
        const isDir = stats.isDirectory();

        if (isFile) {

          // if(!filepath.endsWith('TestHvigorFile.ets')){
          //   return
          // }

          fs.readFile(filepath, 'utf-8', (err, data) => {
            if (err) throw err;

            let content = (data as string)

            extractCodeIndex(content, filepath)

            if(!isDecoratorForComponent(content)){
              console.log('不包含@Component')
              return
            }

            if(isWidget(filepath)){
              console.log('这个文件是用于Widget')
              return
            }

            //开始计算相对路径
            let tempFilePath: string = filepath.substring(codeRootPath.length+1)

            let slashCount: number = 0

            for(let char of tempFilePath){
              if(char == '/'){
                slashCount++
              }
            }

            //导入PageLife.ts文件
            if(configFileName.indexOf('.') != -1){
              configFileName = configFileName.substring(0, configFileName.indexOf('.'))
            }

            let importPath: string = 'import ' + configFileName + ' from ''
            for(let k = 0; k < slashCount; k++){
              importPath += '../'
            }

            if(slashCount == 0){
              importPath += './'
            }

            importPath += configFileName + '''

            content = insertImport(filepath, content, importPath)

            //导入@ohos.app.ability.common
            content = insertImport(filepath, content, "import common from '@ohos.app.ability.common'", '@ohos.app.ability.common')

            content = insertVariable(filepath, content, "private autoContext = getContext(this) as common.UIAbilityContext")

            INSERT_FUNCTION.forEach( funName => {

              console.log('检测 => ' + funName)

              try {

                let dirName = __dirname as string
                let tempFilePath = dirName.substring(dirName.lastIndexOf('/')+1) + filepath.substring(dirName.length)
                content = insertLifecycleFunction(filepath, content, funName, `PageLifecycle.record(this.autoContext, '${tempFilePath}', '${funName}')`)

              } catch (e){
                console.error('L161:' + e)
              }

            })

            //onClick方法插入内容
            content = insertContentToOnClick(content, filepath)

            fs.writeFile(filepath, content, (err) => {
              if (err) throw err;
            });

          });

        }

        if (isDir) {
          findAllFiles(codeRootPath, filepath, configFileName);
        }

      });
    });
  });
}

function isWidget(filepath: string): boolean{
  let checkPages: boolean = false

  let config: string = fs.readFileSync(__dirname + '/src/main/resources/base/profile/form_config.json','utf8');

  let temps = JSON.parse(config)

  filepath.indexOf()
  temps.forms.forEach( (value) => {
    if(filepath.endsWith(value.src.substring(value.src.indexOf('/')+1))){
      checkPages = true
      // console.log(value)
    }
  })

  return checkPages
}

function initAppEntry(codeRootPath: string,  configFileName: string) {
  try{
    console.log('L207:' + __dirname + '/src/main/module.json5')
    let moduleJSON5: string = fs.readFileSync(__dirname + '/src/main/module.json5','utf8');
    let jsonData = JSON.parse(moduleJSON5.trim())
    let srcEntry: string = jsonData.module.srcEntry
    srcEntry = __dirname + '/src/main' +srcEntry.substring(srcEntry.indexOf('/'))
    console.log('L211:' + srcEntry)

    let appEntrySource: string = fs.readFileSync(srcEntry,'utf8');

    let tempFilePath: string = srcEntry.substring(codeRootPath.length+1)

    let slashCount: number = 0

    for(let char of tempFilePath){
      if(char == '/'){
        slashCount++
      }
    }

    //导入PageLife.ts文件
    if(configFileName.indexOf('.') != -1){
      configFileName = configFileName.substring(0, configFileName.indexOf('.'))
    }

    let importPath: string = 'import ' + configFileName + ' from ''
    for(let k = 0; k < slashCount; k++){
      importPath += '../'
    }

    if(slashCount == 0){
      importPath += './'
    }

    importPath += configFileName + '''

    extractCodeIndex(appEntrySource, srcEntry)

    appEntrySource = insertImport(srcEntry, appEntrySource, importPath)

    extractCodeIndex(appEntrySource, srcEntry)
    let onCreateIndex: number = matchTargetString(appEntrySource, 'onCreate', 0)

    let startBrace: number = appEntrySource.indexOf('{', onCreateIndex + 'onCreate'.length)
    let endBrace = findBraceBracket(appEntrySource, startBrace, '{', '}').endIndex

    const INSERT_CONTENT: string = 'PageLifecycle.boot()'

    if(appEntrySource.substring(startBrace, endBrace).indexOf(INSERT_CONTENT) == -1){
      appEntrySource = appEntrySource.substring(0, startBrace + '{'.length)
      +'\n'
      + INSERT_CONTENT
      + appEntrySource.substring(startBrace + '{'.length)
    }

    fs.writeFile(srcEntry, appEntrySource, (err) => {
      if (err) throw err;
    });

  } catch (e){
    console.error('L213:'+e)
  }

}

/**
 * 插入 import ... from '....'
 *
 * @param filepath
 * @param inputContent
 * @param insertContent
 * @param keyContent
 * @returns
 */
function insertImport(filepath: string, inputContent: string, insertContent: string, keyContent?: string): string{
  extractCodeIndex(inputContent, filepath)

  console.log('准备插入Import')

  let position: number = 0

  //不进行强校验
  //比如
  //import
  //kk from 'ss.ss.ss'

  //let imports: number = 0
  let targetIndex: number = matchTargetString(inputContent, 'import ', 0)

  let insertContentIndex: number = inputContent.indexOf(insertContent)

  if(keyContent){
    insertContentIndex = inputContent.indexOf(keyContent)
  }

  //需要插入
  if(insertContentIndex == -1){

    if(targetIndex != -1){
      inputContent = inputContent.substring(0, targetIndex)
      + '\n'
      + insertContent
      + '\n'
      + inputContent.substring(targetIndex)
    } else {
      inputContent = insertContent + '\n' + inputContent
    }

  }

  console.log('插入完成')
  return inputContent
}

/**
 * 是否为@Entry修饰的自定义组件,俗称页面
 * @param inputContent
 * @returns 如果返回true, 则为页面
 */
function isDecoratorForEntry(inputContent: string): boolean{

  console.log('判断文件中是否有Entry')

  return programClassMap.has('@Entry')

}

/**
 * 是否为自定义组件
 * @param inputContent
 * @returns 如果返回true, 则为自定义组件
 *
 */
function isDecoratorForComponent(inputContent: string): boolean{

  console.log('判断文件中是否有Component')

  return programClassMap.has('@Component')

}

//程序结构
let programClassMap: Map<string, CodeIndex[]> = new Map<string, CodeIndex[]>()

const ClassDecorator: string[] = [
  'class',
  'struct',
  '@Entry',
  '@Component',
  '@Observed',
  '@CustomDialog'
]


/**
 * 文件结构化分类
 *
 * @param inputContent
 */
function extractProgramKey(inputContent: string){

  console.log('开始提取文件整体结构')

  //清空
  programClassMap.clear()

  ClassDecorator.forEach( key => {

    let next = 0
    let classIndex: number = -1

    while(true){
      classIndex = matchTargetString(inputContent, key, next)

      let loop: boolean = false

      if(classIndex != -1){
        let preChar: string = ''
        if(classIndex < 1){
          preChar = '\n'
        } else {
          preChar = inputContent.charAt(classIndex - 1)
        }

        let nextChar: string = inputContent.charAt(classIndex + key.length)

        // console.log(key + ' 下一个字符:' + nextChar);

        if(key.startsWith('@')){
          if(nextChar != ' ' && nextChar != '\n' && nextChar != '@'){
            loop = true
          }
        } else {
          if(nextChar != ' ' && (preChar != ' ' || preChar != '\n' )){
            loop = true
          }
        }
      }

      if(loop){

        next = classIndex + key.length

      } else {

        if(classIndex != -1){
          let funStartLabelIndex: number = inputContent.indexOf('{', classIndex + key.length)

          let funEndLabelIndex: number = findBraceBracket(inputContent, funStartLabelIndex, '{', '}').endIndex + 1

          if(!programClassMap.has(key.trim())){
            let codeIndex: CodeIndex [] = []
            codeIndex.push(new CodeIndex(funStartLabelIndex, funEndLabelIndex))
            programClassMap.set(key.trim(), codeIndex)
          } else {
            let codeIndex = programClassMap.get(key.trim())
            codeIndex.push(new CodeIndex(funStartLabelIndex, funEndLabelIndex))
            programClassMap.set(key.trim(), codeIndex)
          }

          console.log(key + ' 被添加了')
          next = funEndLabelIndex + 1

          if(next >= inputContent.length){
            break
          }

        } else {
          break
        }

      }
    }

  })

}

/**
 * 匹配字符串,且字符串不在注释中 \n
 * 这里有种情况例外,比如查找 “@Entry”, 但是有另外一个自定义的 "@EntryCustom", \n
 * 这个时候可能也会命中@EntryCustom
 *
 * @param inputContent
 * @param target
 * @returns
 */
function matchTargetString(inputContent: string, target: string, position: number): number {
  let result: number = -1

  let tempIndex: number = -1

  // console.log('matchTargetString => ' + target)

  try {
    let tempIndex = inputContent.indexOf(target, position)

    while (tempIndex != -1){

      // console.log('查找 - ' +tempIndex)

      if(!isComments(tempIndex)){
        result = tempIndex
        // console.log('正常')
        break
      } else {
        position += target.length
        if(position > inputContent.length){
          break
        }
      }

      tempIndex = inputContent.indexOf(target, position)
    }
  } catch (e){
    console.error('L498:' + e)
  }

  if(result != -1){
    // console.log('L413找到 ' + target)
  }

  return result
}

/**
 * 插入变量
 * @param inputContent
 * @param insertContent
 * @returns
 */
function insertVariable(filepath: string, inputContent: string, insertContent: string): string{

  console.log('L434:准备开始插入上下文变量')
  extractCodeIndex(inputContent, filepath)

  try {
    let codeIndex: CodeIndex[] = programClassMap.get('@Component')
    let loop: number = 0

    while (loop < codeIndex.length){

      let k = codeIndex[loop]

      let variableIndex = inputContent.indexOf(insertContent, k.startIndex)

      if(variableIndex == -1){
        inputContent = inputContent.substring(0, k.startIndex+1) + '\n'  + insertContent + '\n' + inputContent.substring(k.startIndex+1)
      }

      if(codeIndex.length > 1){
        extractCodeIndex(inputContent, filepath)
        codeIndex = programClassMap.get('@Component')
      }

      loop++

    }

  } catch (e){
    console.error('L458:' + e)
  }

  return inputContent
}

/**
 * 生命周期函数插入代码
 * @param inputContent
 * @param funName
 * @param insertContent
 * @returns
 */
function insertLifecycleFunction(filepath: string, inputContent: string, funName: string, insertContent: string): string{


  if(funName == 'onPageShow' || funName == 'onPageHide' || funName == 'onBackPress') {
    try {
      extractCodeIndex(inputContent, filepath)

      let codeIndex: CodeIndex[] = programClassMap.get('@Entry')
      let loop: number = 0

      while (loop < codeIndex.length){

        let k = codeIndex[loop]

        inputContent = insertTargetFunction(filepath, inputContent, funName, insertContent, k)

        if(codeIndex.length > 1){
          extractCodeIndex(inputContent, filepath)

          codeIndex = programClassMap.get('@Entry')
        }

        loop++
      }

    } catch (e){
      console.error('L458:' + e)
    }
  }


  if(funName == 'aboutToAppear' || funName == 'aboutToDisappear') {
    try {
      extractCodeIndex(inputContent, filepath)

      let codeIndex2: CodeIndex[] = programClassMap.get('@Component')
      let loop2: number = 0

      while (loop2 < codeIndex2.length){

        let k2 = codeIndex2[loop2]

        inputContent = insertTargetFunction(filepath, inputContent, funName, insertContent, k2)

        if(codeIndex2.length > 1){

          extractCodeIndex(inputContent, filepath)
          codeIndex2 = programClassMap.get('@Component')
        }

        loop2++
      }

    } catch (e){
      console.error('L521:' + e)
    }
  }

  return inputContent
}


function insertTargetFunction(filepath: string, inputContent: string, funName: string, insertContent: string, k: CodeIndex): string{

  let targetIndex: number = inputContent.indexOf(funName, k.startIndex)

  if((targetIndex != -1) && targetIndex < k.endIndex){
    //生命周期函数已存在
    let funStartLabelIndex: number = inputContent.indexOf('{', targetIndex)

    let funEndLabelIndex: number = findBraceBracket(inputContent, funStartLabelIndex, '{', '}').endIndex

    if(funEndLabelIndex != -1){
      let funContent: string = inputContent.substring(funStartLabelIndex, funEndLabelIndex)

      let insertContentIndex: number = funContent.indexOf(insertContent)

      if(insertContentIndex == -1){
        inputContent = inputContent.substring(0, funStartLabelIndex+1)
        + '\n'
        + insertContent
        + '\n'
        + inputContent.substring(funStartLabelIndex+1)
      }
    }

  } else {
    //生命周期函数不存在
    inputContent = inputContent.substring(0, k.endIndex-1)
    + '\n'
    + funName +'(){'
    + '\n'
    + insertContent
    + '\n'
    + '}'
    + '\n'
    + inputContent.substring(k.endIndex-1)
  }

  return inputContent
}
/**
 * 抽取注释位置
 *
 */
function extractCodeIndex(content: string, filepath: string): boolean{
  console.log('抽取注释:'+filepath)

  if(mulCodeIndex){
    while (mulCodeIndex.length != 0){
      mulCodeIndex.pop()
    }
  }

  if(singleCodeIndex){
    while (singleCodeIndex.length != 0){
      singleCodeIndex.pop()
    }
  }

  if(stringIndex){
    while (stringIndex.length != 0){
      stringIndex.pop()
    }
  }

  let hasComment: boolean = false

  try{
    hasComment = findMulCodeIndex(content, filepath)
    console.log('多行注释抽取完成')
  } catch (e){
    console.error('L566:' + e)
  }

  try {
    hasComment = (hasComment | findSingleCodeIndex(content, filepath))
    console.log('单行注释抽取完成')
  } catch (e){
    console.error('L573:' + e)
  }

  try {
    hasComment = (hasComment | extractStringIndex(content))
    console.log('所有字符串抽取完成')
  } catch (e){
    console.error('L581:' + e)
  }

  try {
    //抽取文件结构
    extractProgramKey(content)
  } catch (e){
    console.error('L588:' + e)
  }

  return hasComment
}

/**
 * 抽取所有字符串的位置
 *
 */
function extractStringIndex(content: string): boolean{
  // stringIndex

  let label: string[] = []
  label.push(''')
  label.push("`")
  label.push(""")

  let commentLabelIndex: CodeIndex[] = []

  let next = 0

  while (true) {

    for (let k = 0; k < label.length; k++) {
      let a = content.indexOf(label[k], next)
      let b = content.indexOf(label[k], a + label.length)
      if (a != -1 && b != -1) {
        //不在注释内
        if (!isComments(a) && !isComments(b)) {
          commentLabelIndex.push(new CodeIndex(a, b))
        }
      }
    }

    if(commentLabelIndex.length == 0){
      break
    }

    //获取最先出现的
    commentLabelIndex = commentLabelIndex.sort((c1, c2)=>{
      return c1.startIndex - c2.startIndex
    })

    let position = commentLabelIndex[0].startIndex;
    let currentChar = content.charAt(position)
    let s = position
    let e = content.indexOf(currentChar, s + currentChar.length)

    //字符串位置信息
    stringIndex.push(new CodeIndex(s, e+currentChar.length))

    // console.log('字符串: ' + content.substring(s, e+currentChar.length))
    if(s != -1 && e != -1 ){
      next = e + 1
    }

    //恢复,重新获取
    while (commentLabelIndex.length != 0){
      commentLabelIndex.pop()
    }

    if(next >= content.length){
      break;
    }

  }

  return stringIndex.length > 0

}

/**
 * onClick方法插入内容
 *
 * @param content
 * @param filepath
 * @returns
 */
function insertContentToOnClick(content: string, filepath: string): string{

  const FUNCTION_NAME: string = '.onClick'
  const INSERT_FUNCTION: string = 'PageLifecycle.click'
  let INSERT_CODE: string = ''

  let position = 0

  let loop: number = 0

  //文件路径参数
  let dirName = __dirname as string
  let tempFilePath = dirName.substring(dirName.lastIndexOf('/')+1) + filepath.substring(dirName.length)

  while (position != -1){
    extractCodeIndex(content, filepath)

    //定位位置
    position = matchTargetString(content, FUNCTION_NAME, position)

    if(position == -1){
      console.log('退出 ' + loop)
      break;
    }

    loop++

    //插入代码
    let startBracket: number = content.indexOf('(', position + FUNCTION_NAME.length)

    let endBracket: number = 0
    let arrowFunctionLabelIndex: number = 0
    let isArrowFunction: boolean = false

    let CodeIndex: CodeIndex = findBraceBracket(content, startBracket, '(', ')')
    endBracket = CodeIndex.endIndex

    let functionBody = content.substring(startBracket,  endBracket+1)
    console.log('L697:' +FUNCTION_NAME + ' : ' + functionBody)

    arrowFunctionLabelIndex = content.indexOf('=>', startBracket)

    if(arrowFunctionLabelIndex != -1 && arrowFunctionLabelIndex < endBracket){
      isArrowFunction = true
    }

    let hasArrowFunctionBrace: boolean = false

    if(isArrowFunction){
      console.log('箭头函数')

      if(functionBody.indexOf(INSERT_FUNCTION) != -1){
        console.log('L720: 不需要新插入代码')
        position = endBracket
        continue
      }

      let braceLeft = content.indexOf('{', arrowFunctionLabelIndex)

      if((braceLeft != -1) && (braceLeft < endBracket)){
        //箭头函数 + 带花括号
        console.log('箭头函数 + 带花括号')
        if( braceLeft < endBracket){

          let preContent = content.substring(0, braceLeft + '{'.length)
          + '\n';

          INSERT_CODE = INSERT_FUNCTION + `('${tempFilePath}', 'Line: ${countLinesNum(preContent)}')`

          content = preContent + INSERT_CODE + content.substring(braceLeft + '{'.length)

        }
      } else {
        //箭头函数 + 无花括号
        console.log('箭头函数 + 无花括号')
        let preContent = content.substring(0, arrowFunctionLabelIndex + '=>'.length)
        + '{'
        + '\n';

        INSERT_CODE = INSERT_FUNCTION + `('${tempFilePath}', '${countLinesNum(preContent)}')`

        content = preContent
        + INSERT_CODE
        + '\n'
        +content.substring(arrowFunctionLabelIndex + '=>'.length, endBracket)
        + '\n}'
        + content.substring(endBracket)
      }

    } else {
      //非箭头函数 =》 不做插入行为
    }

    if(position != -1){
      console.log(FUNCTION_NAME + ' => ' + filepath);
      position++
    }

  }

  return content
}

/**
 * 计算当前内容所占行数
 *
 * @param content
 * @returns
 */
function countLinesNum(content: string): number{

  let count: number = 1
  let index: number = 0

  while(true){
    if(content.charAt(index) == '\n' || content.charAt(index) == '\r'){
      count++
    }
    index++

    if(index >= content.length){
      break
    }
  }

  return count

}

/**
 * 根据字符位置判断是否为注释
 *
 * @param position
 * @returns
 */
function isComments(position: number): boolean{
  let isComment: boolean = false

  if(mulCodeIndex){
    mulCodeIndex.forEach( value => {
      if((value.startIndex <= position)  &&  (position < value.endIndex)){
        // console.log('L646: '+value.startIndex + ' -> ' + position + ' -> ' + value.endIndex)
        isComment = true
      }
    })
  }

  if(singleCodeIndex){
    singleCodeIndex.forEach( value => {
      if((value.startIndex <= position)  &&  (position < value.endIndex)){
        // console.log('L655: ' + value.startIndex + ' -> ' + position + ' -> ' + value.endIndex)
        isComment = true
      }
    })
  }

  if(stringIndex){
    stringIndex.forEach( value => {
      if((value.startIndex <= position)  &&  (position < value.endIndex)){
        // console.log('L655: ' + value.startIndex + ' -> ' + position + ' -> ' + value.endIndex)
        isComment = true
      }
    })
  }

  return isComment

}

/**
 * 查找所有的多行注释
 * @param inputContent
 * @returns
 */
function findMulCodeIndex(inputContent: string, filePath: string): boolean{

  console.log('抽取多行注释')

  let hasComment: boolean = false

  try{
    let mulLinesStart = 0
    let mulLinesEnd = 0

    const mulCodeIndexPre: string = '/*'
    const mulCodeIndexEnd: string = '*/'

    while (true){
      mulLinesStart = inputContent.indexOf(mulCodeIndexPre, mulLinesStart)

      if(mulLinesStart != -1){
        mulLinesEnd = inputContent.indexOf(mulCodeIndexEnd, mulLinesStart+mulCodeIndexPre.length)
        if(mulLinesEnd != -1){

          let comment = new CodeIndex(mulLinesStart, mulLinesEnd + mulCodeIndexEnd.length)
          mulCodeIndex.push(comment)
          console.log('L745: '+inputContent.substring(comment.startIndex, comment.endIndex))

          mulLinesStart = mulLinesEnd
          hasComment = true

        } else {
          mulLinesStart += 1
          if(mulLinesStart >= inputContent.length){
            break
          }
        }

      } else {
        break
      }

    }
  } catch (e){
    console.error('L912:'+e)
  }

  return hasComment

}

/**
 * 查找单行注释
 * @param inputContent
 * @returns
 */
function findSingleCodeIndex(inputContent: string, filepath: string): boolean{

  console.log('抽取单行注释')

  let currentLineStartPosition: number = 0

  let splitContent = inputContent.split(/\r?\n/)

  let hasComment: boolean = false

  splitContent.forEach( value => {
    // console.log('输入>>' + value)

    let tempValue = value.trim()

    //第一种注释, 单行后边没有跟注释
    // m = 6
    if(tempValue.indexOf('//') == -1){
      // if(tempValue.length != 0){
      //   inputContent = inputContent + value + '\n'
      // }
      //第二种注释,一整行都为注释内容
      //这是一个演示注释
    } else if(tempValue.startsWith('//')) {

      let s = currentLineStartPosition + value.indexOf('//')
      let e = currentLineStartPosition + value.length
      let includeSingle: boolean = false

      if(mulCodeIndex){
        mulCodeIndex.forEach( comment => {
          if( (comment.startIndex < s ) && (comment.endIndex > e)) {
            includeSingle = true
          }
        })
      }

      if(!includeSingle){
        hasComment = true
        console.log('L650: singleCodeIndex='+singleCodeIndex)
        singleCodeIndex.push(new CodeIndex(s, e))
        console.log('L887单行注释:' + inputContent.substring(s, e))
      }

    } else {

      //第三种注释
      // m = 'h//' + "//ell" + `o` //https://www.baidu.com

      let lineContentIndex = -1

      let next: number = 0

      let label: string[] = []
      label.push(''')
      label.push("`")
      label.push(""")

      let commentLabelIndex: CodeIndex[] = []

      while (true) {

        let guessCommentIndex: number = value.indexOf('//', next)

        for(let k = 0; k < label.length; k++){
          let a = value.indexOf(label[k], next)
          let b = value.indexOf(label[k], a + label[k].length)
          if(a != -1 && b != -1){
            if((guessCommentIndex > b) || ((guessCommentIndex > a) && guessCommentIndex < b)){
              commentLabelIndex.push(new CodeIndex(a, b))
            }
          }
        }

        //第四种注释
        // m = 2 //这是一个演示注释
        if(commentLabelIndex.length == 0){
          // console.log('单行注释 m=2 :' + value)
          // console.log('next='+next)
          if(value.indexOf('//', next) != -1){
            let s = currentLineStartPosition + value.indexOf('//')
            let e = currentLineStartPosition + value.length
            let includeSingle2: boolean = false

            mulCodeIndex.forEach( value => {
              // console.log('检查是否存在于多行注释:' + value.startIndex + '-' + value.endIndex +' =>' + s)
              if( (value.startIndex < s ) && (value.endIndex > e)) {
                //完全在多行注释中

                includeSingle2 = true
                // console.log('存在于多行注释中')
              } else if( (value.startIndex < s) && ( s == (value.endIndex -1))) {
                //部分在多行注释中
                //比如: /***///test
                if(!isComments(s+1)){
                  s++
                  // console.log('部分存在于多行注释中 ')
                } else {
                  includeSingle2 = true
                  // console.log('存在于多行注释中2')
                }
              }

            })

            if(!includeSingle2){
              hasComment = true

              singleCodeIndex.push(new CodeIndex(s, e))
              console.log('L941单行注释:' + inputContent.substring(s, e))
            }

          }

          break

        } else {

          //获取最先出现的
          commentLabelIndex = commentLabelIndex.sort((c1, c2)=>{
            return c1.startIndex - c2.startIndex
          })

          let position = commentLabelIndex[0].startIndex;
          let currentChar = value.charAt(position)
          let s = value.indexOf(currentChar, next)
          let e = value.indexOf(currentChar, s + currentChar.length)

          // console.log('currentChar='+currentChar + ' s='+s+' e='+e)
          if(s != -1 && e != -1 ){
            next = e + 1
          }

          //恢复,重新获取
          while (commentLabelIndex.length != 0){
            commentLabelIndex.pop()
          }

        }

      }
    }

    currentLineStartPosition = currentLineStartPosition + value.length + 1 //1:代表换行符

  })

  while (splitContent.length != 0){
    splitContent.pop()
  }
  splitContent = null

  return hasComment

}

/**
 * 查找匹配对
 *
 * 例如: (),{}
 * @param inputContent
 * @param currentIndex
 * @param pre
 * @param end
 * @returns
 */
function findBraceBracket(inputContent: string, currentIndex: number, pre: string, end: string): CodeIndex{
  let computer: CodeIndex = new CodeIndex()
  computer.startIndex = currentIndex

  let count: number = 0

  if(currentIndex != -1){
    count++
    currentIndex++
  }

  let tempChar: string = ''

  while(count != 0){
    let findNext: boolean = isComments(currentIndex)

    if(!findNext){

      tempChar = inputContent.charAt(currentIndex)

      if(tempChar == end){
        count--
      } else if(tempChar == pre){
        count++
      }

      // console.log('count = ' + count)
      if(count == 0){
        computer.endIndex = currentIndex
        break
      }

    }

    currentIndex++

    if(currentIndex >= inputContent.length){
      break;
    }

  }

  return computer

}

/**
 * 代码起始位置
 *
 */
class CodeIndex{
  public startIndex: number = 0
  public endIndex: number = 0

  constructor(s: number, e: number) {
    this.startIndex = s
    this.endIndex = e
  }
}
  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值