diff --git a/BUILD.gn b/BUILD.gn index 9d8bbec..0535ce6 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -17,6 +17,8 @@ import("//foundation/ace/ace_engine/ace_config.gni") ets_loader_lib_dir = get_label_info(":build_ets_loader_library", "target_out_dir") + "/lib" +ets_loader_syntax_dir = get_label_info(":build_ets_loader_library", + "target_out_dir") + "/syntax_parser/dist" ets_loader_declarations_dir = get_label_info(":build_ets_loader_library", "target_out_dir") + "/declarations" ets_loader_component_config_file = @@ -30,6 +32,7 @@ action("build_ets_loader_library") { depfile = "$target_gen_dir/$target_name.d" outputs = [ ets_loader_lib_dir, + ets_loader_syntax_dir, ets_loader_declarations_dir, ets_loader_component_config_file, ] @@ -45,12 +48,14 @@ action("build_ets_loader_library") { _babel_js = _ace_config_dir + "/node_modules/@babel/cli/bin/babel.js" _babel_config_js = _ace_config_dir + "/babel.config.js" _uglify_source_js = _ace_config_dir + "/uglify-source.js" + _build_parser_js = _ace_config_dir + "/build_parser.js" _build_declarations_file_js = _ace_config_dir + "/build_declarations_file.js" inputs = [ _babel_config_js, _babel_js, _uglify_source_js, + _build_parser_js, _build_declarations_file_js, ] @@ -82,12 +87,18 @@ action("build_ets_loader_library") { rebase_path(_babel_js, root_build_dir), "--ets-loader-src-dir", rebase_path(_ets_loader_dir + "/src", root_build_dir), + "--ets-loader-syntax-src-dir", + rebase_path(_ets_loader_dir + "/syntax_parser/src", root_build_dir), "--babel-config-js", rebase_path(_babel_config_js, root_build_dir), "--uglify-source-js", rebase_path(_uglify_source_js, root_build_dir), + "--build-parser-js", + rebase_path(_build_parser_js, root_build_dir), "--output-dir", rebase_path(ets_loader_lib_dir, root_build_dir), + "--output-syntax-dir", + rebase_path(ets_loader_syntax_dir, root_build_dir), "--declarations-file-dir", rebase_path(_declarations_file_dir, root_build_dir), "--build-declarations-file-js", @@ -143,6 +154,14 @@ ohos_copy("ets_loader_library") { module_install_name = "" } +ohos_copy("ets_loader_syntax") { + deps = [ ":build_ets_loader_library" ] + sources = [ ets_loader_syntax_dir ] + outputs = [ target_out_dir + "/$target_name" ] + module_source_dir = target_out_dir + "/$target_name" + module_install_name = "" +} + ohos_copy("ets_loader_declaration") { deps = [ ":build_ets_loader_library" ] sources = [ ets_loader_declarations_dir ] diff --git a/build_ets_loader_library.py b/build_ets_loader_library.py index d160556..4511664 100755 --- a/build_ets_loader_library.py +++ b/build_ets_loader_library.py @@ -39,9 +39,14 @@ def parse_args(): parser.add_argument('--node', help='path to nodejs exetuable') parser.add_argument('--babel-js', help='path to babel.js') parser.add_argument('--ets-loader-src-dir', help='path to compiler/src') + parser.add_argument('--ets-loader-syntax-src-dir', + help='path to compiler/syntax_parser/src') parser.add_argument('--babel-config-js', help='path babel.config.js') parser.add_argument('--uglify-source-js', help='path uglify-source.js') + parser.add_argument('--build-parser-js', help='path build_parser.js') parser.add_argument('--output-dir', help='path to output') + parser.add_argument('--output-syntax-dir', + help='path syntax file to output') parser.add_argument('--declarations-file-dir', help='path declarations file') parser.add_argument('--build-declarations-file-js', @@ -55,8 +60,9 @@ def parse_args(): return options -def do_build(build_cmd, uglify_cmd, build_declarations_file_cmd): - for cmd in [build_cmd, uglify_cmd, build_declarations_file_cmd]: +def do_build(build_cmd, uglify_cmd, syntax_cmd, build_declarations_file_cmd): + for cmd in [build_cmd, uglify_cmd, syntax_cmd, + build_declarations_file_cmd]: build_utils.check_output(cmd) @@ -70,10 +76,16 @@ def main(): build_cmd.extend(['--config-file', options.babel_config_js]) depfile_deps = [options.node, options.babel_js, options.babel_config_js] depfile_deps.extend(build_utils.get_all_files(options.ets_loader_src_dir)) + depfile_deps.extend( + build_utils.get_all_files(options.ets_loader_syntax_src_dir)) uglify_cmd = [options.node, options.uglify_source_js, options.output_dir] depfile_deps.append(options.uglify_source_js) + syntax_cmd = [options.node, options.build_parser_js, + options.output_syntax_dir] + depfile_deps.append(options.build_parser_js) + build_declarations_file_cmd = [options.node, options.build_declarations_file_js, options.declarations_file_dir, @@ -82,12 +94,14 @@ def main(): depfile_deps.append(options.build_declarations_file_js) build_utils.call_and_write_depfile_if_stale( - lambda: do_build(build_cmd, uglify_cmd, build_declarations_file_cmd), + lambda: do_build(build_cmd, uglify_cmd, + syntax_cmd, build_declarations_file_cmd), options, depfile_deps=depfile_deps, input_paths=depfile_deps, output_paths=([ options.output_dir, + options.output_syntax_dir, options.output_declarations_dir, options.output_component_config_file])) diff --git a/compiler/build_parser.js b/compiler/build_parser.js new file mode 100644 index 0000000..03f79ad --- /dev/null +++ b/compiler/build_parser.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const util = require('util'); +const childProcess = require('child_process'); +const exec = util.promisify(childProcess.exec); +const fs = require('fs'); +const path = require('path'); + +function generateSyntaxParser(inputFile, nodePath) { + const readDirPath = path.resolve(__dirname, './syntax_parser/src'); + const readDirSubFiles = fs.readdirSync(readDirPath); + const catalogPath = path.resolve(inputFile, '..'); + const catalogSubFiles = fs.readdirSync(catalogPath) + const parserPath = path.resolve(__dirname, './node_modules/pegjs/bin/pegjs'); + + if (catalogSubFiles.includes('dist')) { + exec('rm -rf ' + catalogPath + '/dist/*.js'); + } else { + exec('mkdir ' + catalogPath + '/dist'); + } + + ;(async function transJs () { + if (readDirSubFiles.length) { + for (let item of readDirSubFiles) { + let name = path.basename(item, '.peg'); + if (name){ + await exec(nodePath + ' ' + parserPath + ' -o ' + + catalogPath + '/dist/' + name + '.js ' + readDirPath + '/' + item); + } + } + } + })() +} + +generateSyntaxParser(process.argv[2], process.argv[0]); diff --git a/compiler/package-lock.json b/compiler/package-lock.json index de1d850..7fa081a 100644 --- a/compiler/package-lock.json +++ b/compiler/package-lock.json @@ -4295,6 +4295,11 @@ "integrity": "sha1-hTTnenfOesWiUS6iHg/bj89sPY0=", "dev": true }, + "pegjs": { + "version": "0.10.0", + "resolved": "https://registry.npm.taobao.org/pegjs/download/pegjs-0.10.0.tgz", + "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=" + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.nlark.com/picomatch/download/picomatch-2.3.0.tgz", diff --git a/compiler/package.json b/compiler/package.json index 1254ed9..fe0cde3 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -10,11 +10,12 @@ ], "scripts": { "lint": "eslint --fix ./src --ext .ts", - "build": "npm run generateDeclarations && ./node_modules/.bin/babel ./src --out-dir lib --extensions .ts", + "build": "npm run generateSyntaxParser && npm run generateDeclarations && ./node_modules/.bin/babel ./src --out-dir lib --extensions .ts", "create": "node ./lib/create.js --env projectName", "compile": "webpack --config webpack.config.js --env buildMode=debug projectName", "test": "npm run build && mocha -r ts-node/register test/test.ts", "generateDeclarations": "node ./build_declarations_file.js ../../../interface/sdk-js/api/@internal/component/ets ./declarations ./lib", + "generateSyntaxParser": "node build_parser.js ./syntax_parser/dist", "postinstall": "node npm-install.js" }, "devDependencies": { @@ -36,6 +37,7 @@ "copy-webpack-plugin": "^8.1.0", "deccjsunit": "1.0.3", "log4js": "^6.3.0", + "pegjs": "^0.10.0", "ts-loader": "^8.0.12", "typescript": "^4.1.3", "webpack": "^5.48.0", diff --git a/compiler/src/pre_process.ts b/compiler/src/pre_process.ts index 752cd12..a95b88b 100644 --- a/compiler/src/pre_process.ts +++ b/compiler/src/pre_process.ts @@ -14,6 +14,7 @@ */ import { + ReplaceResult, sourceReplace, validateUISyntax, processSystemApi @@ -27,9 +28,10 @@ import { BUILD_ON } from './pre_define'; function preProcess(source: string): string { process.env.compiler = BUILD_ON; if (/\.ets$/.test(this.resourcePath)) { - const newContent: string = sourceReplace(source); - const log: LogInfo[] = - validateUISyntax(source, newContent, this.resourcePath, this.resourceQuery); + const result: ReplaceResult = sourceReplace(source, this.resourcePath); + const newContent: string = result.content; + const log: LogInfo[] = result.log; + log.concat(validateUISyntax(source, newContent, this.resourcePath, this.resourceQuery)); if (log.length) { emitLogInfo(this, log); } diff --git a/compiler/src/validate_ui_syntax.ts b/compiler/src/validate_ui_syntax.ts index fa852ad..5759496 100644 --- a/compiler/src/validate_ui_syntax.ts +++ b/compiler/src/validate_ui_syntax.ts @@ -59,6 +59,7 @@ import { addLog, hasDecorator } from './utils'; +const parser = require('../syntax_parser/dist/syntax_parser.js'); export interface ComponentCollection { entryComponent: string; @@ -683,9 +684,14 @@ function collectionStates(decorator: string, name: string, states: Set, } } -export function sourceReplace(source: string): string { - let content: string = source; +export interface ReplaceResult { + content: string, + log: LogInfo[] +} +export function sourceReplace(source: string, sourcePath: string): ReplaceResult { + let content: string = source; + const log: LogInfo[] = []; // replace struct->class content = content.replace( new RegExp('\\b' + STRUCT + '\\b.+\\{', 'g'), item => { @@ -693,28 +699,52 @@ export function sourceReplace(source: string): string { return `${item} constructor(${COMPONENT_CONSTRUCTOR_ID}?, ${COMPONENT_CONSTRUCTOR_PARENT}?, ${COMPONENT_CONSTRUCTOR_PARAMS}?) {}`; }); - content = preprocessExtend(content); - content = preprocessNewExtend(content); + content = preprocessExtend(content, sourcePath, log); // process @system. content = processSystemApi(content); - return content; + return { + content: content, + log: log + } } -export function preprocessExtend(content: string): string { - const REG_EXTEND: RegExp = /(? { - collectExtend(item3, item4, item6); - return `${item1}${COMPONENT_EXTEND_DECORATOR} function${item2}__${item3}__${item4}${item5}${item6}${item7}${item3}`; - }); -} - -export function preprocessNewExtend(content: string): string { - const REG_EXTEND: RegExp = /(? { - collectExtend(item2, item5, item7); - return `${item1}${COMPONENT_EXTEND_DECORATOR}${item3}function${item4}__${item2}__${item5}${item6}${item7}${item8}${item2}`; - }); +export function preprocessExtend(content: string, sourcePath: string, log: LogInfo[]): string { + let syntaxCheckContent: string; + let result: any; + try { + result = parser.parse(content); + syntaxCheckContent = result.content; + for (let i = 0; i < result.collect_extend.component.length; i++) { + collectExtend( + result.collect_extend.component[i], + result.collect_extend.functionName[i], + result.collect_extend.parameters[i] + ); + } + } catch (err) { + result = err; + log.push({ + type: LogType.ERROR, + message: parser.SyntaxError.buildMessage(err.expected, err.found), + line: err.location.start.line, + column: err.location.start.column, + fileName: sourcePath + }); + syntaxCheckContent = content; + } + if (result.error_otherParsers) { + for(let i = 0; i < result.error_otherParsers.length; i++) { + log.push({ + type: LogType.ERROR, + message: result.error_otherParsers[i].errMessage, + line: result.error_otherParsers[i].errPosition.line, + column: result.error_otherParsers[i].errPosition.column, + fileName: sourcePath + }); + } + } + return syntaxCheckContent; } function collectExtend(component: string, attribute: string, parameter: string): void { diff --git a/compiler/syntax_parser/src/parse_extend.peg b/compiler/syntax_parser/src/parse_extend.peg new file mode 100644 index 0000000..0dcec46 --- /dev/null +++ b/compiler/syntax_parser/src/parse_extend.peg @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +start = extendAttributes:extendAttribute+ +{ + return {location: location()}; +} + +extendAttribute = whiteSpace '.' whiteSpace attribute item +attribute = attr_head attr_tail +attr_head = $ [a-zA-Z_$]+ +attr_tail = $ [a-zA-Z0-9_$]* +item = whiteSpace '(' [^()]* item* [^()]* ')' whiteSpace +whiteSpace = [ \t\r\n]* diff --git a/compiler/syntax_parser/src/syntax_parser.peg b/compiler/syntax_parser/src/syntax_parser.peg new file mode 100644 index 0000000..c77c99a --- /dev/null +++ b/compiler/syntax_parser/src/syntax_parser.peg @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + const parse_extend = require('./parse_extend.js'); + + const error_otherParsers = []; + let singleError = function(errMessage, line, column) { + this.errMessage = errMessage; + this.errPosition = { + line: line, + column: column + }; + }; + const collect_extend = { + component: [], + functionName: [], + parameters: [] + }; +} + +start = blocks: block+ +{ + return { + content: blocks.join(''), + location: location(), + collect_extend: collect_extend, + error_otherParsers: error_otherParsers + }; +} + +block = extend/(prefix extend?) +prefix = $((!prefix_extend .)+) +extend = + whiteSpace1:whiteSpace prefix_extend:prefix_extend funcBody_extend:funcBody_extend + whiteSpace2: whiteSpace right:'}' whiteSpace3:whiteSpace +{ + return whiteSpace1 + prefix_extend + funcBody_extend + whiteSpace2 + right + whiteSpace3; +} + +prefix_extend = prefix_extend_one/prefix_extend_another + +//match a kind of extend component +prefix_extend_one = + '@Extend' whiteSpace1:whiteSpace '(' component:component ')' whiteSpace2:whiteSpace 'function' + whiteSpace3:whiteSpace function_name:function_name whiteSpace4:whiteSpace '(' parameters:parameters + ')' whiteSpace5:whiteSpace left:'{' +{ + collect_extend.component.push(component); + collect_extend.functionName.push(function_name); + collect_extend.parameters.push(parameters); + return '@Extend' + whiteSpace1 + whiteSpace2 + 'function' + whiteSpace3 + '__' + component + + '__' + function_name + whiteSpace4 + '(' + parameters + ')' + whiteSpace5 + left + component; +} + +//match another kind of extend component +prefix_extend_another = + '@Extend' whiteSpace1:whiteSpace component:component '.' function_name:function_name + whiteSpace2:whiteSpace '(' parameters:parameters ')' whiteSpace3:whiteSpace left:'{' +{ + collect_extend.component.push(component); + collect_extend.functionName.push(function_name); + collect_extend.parameters.push(parameters); + return '@Extend' + ' function' + whiteSpace1 + '__' + component + '__' + function_name + + whiteSpace2 + '(' + parameters + ')' + whiteSpace3 + left + component; +} + +component = $ [a-zA-Z_]+ +function_name = $ (function_name_head function_name_tail) +function_name_head = $ [a-zA-Z_$]+ +function_name_tail = $ [a-zA-Z0-9_$]* +parameters = $ ([^()]* item* [^()]*) + +//extract Extend internal attribute SyntaxError +funcBody_extend = body:($[^{}]* item* [^{}]*) +{ + try { + let functionBody = body.join(''); + parse_extend.parse(functionBody); + return functionBody; + } catch (err) { + let countLines = location().end.line - location().start.line; + if (err.location.start.line === 1) { + err.location.start.column += location().start.column; + } + let supportMessage = " Our rule is .attribute(value) , for example: .width(50) . And you'd better have at least one attribute in Extend Component"; + error_otherParsers.push( + new singleError( + parse_extend.SyntaxError.buildMessage(err.expected, err.found) + supportMessage, + err.location.start.line + location().start.line - 1, + err.location.start.column + ) + ); + let placeHolders = ''; + while (countLines>0) { + placeHolders += '\r\n'; + countLines -= 1; + } + return '()' + placeHolders; + } +} + +item = $ (whiteSpace '{' [^{}]* item* [^{}]* '}' whiteSpace) +whiteSpace = $ [ \t\r\n]*