背景
关于脚本埋点,在上一篇文章有所实践,当时主要是验证一下脚本动态修改源码埋点的可行性,结论是可行的。
本篇是对完善之后的脚本做个总结。
初衷
为实现基本页面生命周期追踪和点击行为追踪,能够快捷的在项目中实现基础数据采集。
可惜HarmonyOS没有对应用开发者提供自定义transformer的功能
在TypeScript语言中,可以采用自定义装饰器和AST(Abstract Syntax Tree)两种方式来提高基础埋点效率。
备注
实践下来,采用AST方式操纵代码修改,暂时能想到的方法就是另外开一个工程,专门用于自动化源码埋点
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中,源码标识字符串的标识总共有三种
- '
- "
- `
例如
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种,
- @Entry
- @Component
- @Observed
- @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个基础数据
- 行为序列编号(即 每次从主模块主入口进入应用时的标识)
- 页面所属模块
- 页面所在的容器
- 页面所在的文件
- 生命周期名称
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个基础数据
- 行为序列编号(即 每次从主模块主入口进入应用时的标识)
- 事件被哪个文件消耗
- 事件被哪个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 编译之后的工程源文件效果
4.3 DevEco Studio 中日志效果
4.4 脚本源文件
- 脚本源码需直接 hvigorfile.ts 文件中
- 埋点源文件,需放在工程根目录下的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
}
}
结尾
埋点不是什么重点,“.hvigor/project_caches” 这个隐藏文件夹中的@ohos文件夹文件比较有意思。
当前这种埋点对于测试人员也是比较有利。
祝各位好运!