Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit dc0594f

Browse files
Add new cli for bf lg:analyze (#1151)
* add cli for bf lg:analyze * typo * fix typo * refactor to use usual syntax * refactor if else * trigger CI Co-authored-by: Emilio Munoz <[email protected]>
1 parent b9b652d commit dc0594f

File tree

13 files changed

+301
-10
lines changed

13 files changed

+301
-10
lines changed

packages/cli/README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ $ npm install -g @microsoft/botframework-cli
3838
* [`bf lg:expand`](#bf-lgexpand)
3939
* [`bf lg:translate`](#bf-lgtranslate)
4040
* [`bf lg:verify`](#bf-lgverify)
41+
* [`bf lg:analyze`](#bf-lganalyze)
4142
* [`bf luis`](#bf-luis)
4243
* [`bf luis:application:assignazureaccount`](#bf-luisapplicationassignazureaccount)
4344
* [`bf luis:application:create`](#bf-luisapplicationcreate)
@@ -455,11 +456,29 @@ OPTIONS
455456
-h, --help lg:verify help
456457
-i, --in=in (required) Folder that contains .lg file.
457458
-o, --out=out Output file or folder name. If not specified stdout will be used as output
458-
-r, --recurse Considere sub-folders to find .lg file(s)
459+
-r, --recurse Considers sub-folders to find .lg file(s)
459460
```
460461

461462
_See code: [@microsoft/bf-lg-cli](https://github.com/microsoft/botframework-cli/tree/master/packages/lg/src/commands/lg/verify.ts)_
462463

464+
## `bf lg:analyze`
465+
466+
Analyze templates in .lg files to show all the places where a template is used.
467+
468+
```
469+
USAGE
470+
$ bf lg:analyze
471+
472+
OPTIONS
473+
-f, --force If --out flag is provided with the path to an existing file, overwrites that file
474+
-h, --help lg:analyze help
475+
-i, --in=in (required) LG File or folder that contains .lg file(s)
476+
-o, --out=out Output file or folder name. If not specified stdout will be used as output
477+
-r, --recurse Considers sub-folders to find .lg file(s)
478+
```
479+
480+
_See code: [src/commands/lg/verify.ts](https://github.com/microsoft/botframework-cli/tree/master/packages/lg/src/commands/lg/analyze.ts)_
481+
463482
## `bf luis`
464483

465484
Manages LUIS assets on service and/or locally.

packages/lg/.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"no-await-in-loop": "off",
99
"no-negated-condition": "off",
1010
"max-params": "off",
11+
"@typescript-eslint/consistent-type-assertions": "off",
1112
"file-header": [
1213
true,
1314
{

packages/lg/README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This package is intended for Microsoft use only and should be consumed through @
1717
* [`bf lg:expand`](#bf-lgexpand)
1818
* [`bf lg:translate`](#bf-lgtranslate)
1919
* [`bf lg:verify`](#bf-lgverify)
20+
* [`bf lg:analyze`](#bf-lganalyze)
2021

2122
## `bf lg`
2223

@@ -92,10 +93,28 @@ OPTIONS
9293
-h, --help lg:verify help
9394
-i, --in=in (required) Folder that contains .lg file.
9495
-o, --out=out Output file or folder name. If not specified stdout will be used as output
95-
-r, --recurse Considere sub-folders to find .lg file(s)
96+
-r, --recurse Considers sub-folders to find .lg file(s)
9697
```
9798

9899
_See code: [src/commands/lg/verify.ts](https://github.com/microsoft/botframework-cli/tree/master/packages/lg/src/commands/lg/verify.ts)_
100+
101+
## `bf lg:analyze`
102+
103+
Analyze templates in .lg files to show all the places where a template is used.
104+
105+
```
106+
USAGE
107+
$ bf lg:analyze
108+
109+
OPTIONS
110+
-f, --force If --out flag is provided with the path to an existing file, overwrites that file
111+
-h, --help lg:analyze help
112+
-i, --in=in (required) LG File or folder that contains .lg file(s)
113+
-o, --out=out Output file or folder name. If not specified stdout will be used as output
114+
-r, --recurse Consider sub-folders to find .lg file(s)
115+
```
116+
117+
_See code: [src/commands/lg/verify.ts](https://github.com/microsoft/botframework-cli/tree/master/packages/lg/src/commands/lg/analyze.ts)_
99118
<!-- commandsstop -->
100119

101120
[1]:https://aka.ms/lg-file-format

packages/lg/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"@microsoft/bf-cli-command": "1.0.0",
1111
"@oclif/command": "^1.5.19",
1212
"@oclif/config": "^1.14.0",
13-
"botbuilder-lg":"4.8.0-preview",
13+
"botbuilder-lg":"4.12.0",
1414
"delay": "^4.3.0",
1515
"fs-extra": "^8.1.0",
1616
"lodash": "^4.17.15",
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* @module @microsoft/bf-lg-cli
3+
*/
4+
/**
5+
* Copyright (c) Microsoft Corporation. All rights reserved.
6+
* Licensed under the MIT License.
7+
*/
8+
9+
import {Command, flags, CLIError} from '@microsoft/bf-cli-command'
10+
import {Helper} from '../../utils'
11+
import {Templates, DiagnosticSeverity, Diagnostic} from 'botbuilder-lg'
12+
import * as path from 'path'
13+
import * as fs from 'fs-extra'
14+
15+
const NEWLINE = require('os').EOL
16+
17+
type FullTemplateName = string
18+
type TemplateName = string
19+
type Source = string
20+
type SourceToReferences = Map<Source, TemplateName[]>
21+
type TemplateToReferences = Map<FullTemplateName, SourceToReferences>
22+
23+
export default class AnalyzeCommand extends Command {
24+
static description = 'Analyze templates in .lg files to show all the places where a template is used'
25+
26+
static flags: flags.Input<any> = {
27+
in: flags.string({char: 'i', description: 'LG File or folder that contains .lg file(s)', required: true}),
28+
recurse: flags.boolean({char: 'r', description: 'Consider sub-folders to find .lg file(s)'}),
29+
out: flags.string({char: 'o', description: 'Output file or folder name. If not specified stdout will be used as output'}),
30+
force: flags.boolean({char: 'f', description: 'If --out flag is provided with the path to an existing file, overwrites that file'}),
31+
help: flags.help({char: 'h', description: 'lg:analyze help'}),
32+
}
33+
34+
async run() {
35+
const {flags} = this.parse(AnalyzeCommand)
36+
37+
const lgFilePaths = Helper.findLGFiles(flags.in, flags.recurse)
38+
39+
Helper.checkInputAndOutput(lgFilePaths)
40+
41+
const allTemplates = []
42+
for (const filePath of lgFilePaths) {
43+
const templates = Templates.parseFile(filePath)
44+
this.checkDiagnostics(templates.allDiagnostics)
45+
allTemplates.push(templates)
46+
}
47+
48+
const templateToReferences = this.templateUsage(allTemplates)
49+
await this.writeTemplateReferences(templateToReferences, flags.out, flags.force)
50+
}
51+
52+
private templateUsage(templates: Templates[]): TemplateToReferences {
53+
const usage = new Map<FullTemplateName, SourceToReferences>()
54+
const analyzed = new Set<Source>()
55+
for (const source of templates) {
56+
// Map from simple to full name and initialize template usage
57+
const nameToFullname = new Map<string, string>()
58+
for (const template of source.allTemplates) {
59+
const fullName = `${template.sourceRange.source}:${template.name}`
60+
nameToFullname.set(template.name, fullName)
61+
if (!usage.get(fullName)) {
62+
usage.set(fullName, new Map<Source, TemplateName[]>())
63+
}
64+
}
65+
66+
// Add references from each template that is in an unanalyzed source file
67+
for (const template of source.allTemplates) {
68+
// Analyze each original source template only once
69+
if (!analyzed.has(template.sourceRange.source)) {
70+
const info = source.analyzeTemplate(template.name)
71+
for (const reference of info.TemplateReferences) {
72+
const source = template.sourceRange.source as string
73+
const referenceSources = usage.get(nameToFullname.get(reference) as string) as Map<string, string[]>
74+
let referenceSource = referenceSources.get(source)
75+
if (!referenceSource) {
76+
referenceSource = []
77+
referenceSources.set(source, referenceSource)
78+
}
79+
referenceSource.push(template.name)
80+
}
81+
}
82+
}
83+
84+
// Add in the newly analyzed sources
85+
for (const imported of source.imports) {
86+
analyzed.add(path.resolve(path.dirname(source.source), imported.id))
87+
}
88+
analyzed.add(source.source)
89+
}
90+
return usage
91+
}
92+
93+
private async writeTemplateReferences(templateToReferences: TemplateToReferences, out: string, force: boolean) {
94+
if (templateToReferences !== undefined && templateToReferences.size >= 0) {
95+
const analysisContent = this.generateAnalysisResult(templateToReferences)
96+
if (out) {
97+
// write to file
98+
const outputFilePath = this.getOutputFile(out)
99+
Helper.writeContentIntoFile(outputFilePath, analysisContent, force)
100+
this.log(`Analysis result have been written into file ${outputFilePath}`)
101+
} else {
102+
// write to console
103+
this.log(analysisContent)
104+
}
105+
} else {
106+
this.log('No analysis result')
107+
}
108+
}
109+
110+
private checkDiagnostics(diagnostics: Diagnostic[]) {
111+
const errors = diagnostics.filter(u => u.severity === DiagnosticSeverity.Error)
112+
if (errors && errors.length > 0) {
113+
throw new CLIError(errors.map(u => u.toString()).join('\n'))
114+
}
115+
116+
const warnings = diagnostics.filter(u => u.severity === DiagnosticSeverity.Warning)
117+
if (warnings && warnings.length > 0) {
118+
this.warn(warnings.map(u => u.toString()).join('\n'))
119+
}
120+
}
121+
122+
private getOutputFile(out: string): string {
123+
const base = Helper.normalizePath(path.resolve(out))
124+
const root = path.dirname(base)
125+
if (!fs.existsSync(root)) {
126+
throw new Error(`Folder ${root} not exist`)
127+
}
128+
129+
const extension = path.extname(base)
130+
if (extension) {
131+
// file
132+
return base
133+
}
134+
135+
// folder
136+
// default to analysisResult.txt
137+
return path.join(base, 'analysisResult.txt')
138+
}
139+
140+
private templateName(fullname: string): string {
141+
const colon = fullname.lastIndexOf(':')
142+
return fullname.substring(colon + 1)
143+
}
144+
145+
private shortTemplateName(fullname: string): string {
146+
const colon = fullname.lastIndexOf(':')
147+
return path.basename(fullname.substring(0, colon)) + fullname.substring(colon)
148+
}
149+
150+
private generateAnalysisResult(templateToReferences: TemplateToReferences): string {
151+
let result = ''
152+
for (const templateToReference of templateToReferences) {
153+
const shortTemplateName = this.shortTemplateName(templateToReference[0])
154+
result += `${shortTemplateName} references:${NEWLINE}`
155+
const references = templateToReference[1]
156+
for (const reference of references) {
157+
result += ` ${path.basename(reference[0])}: ${reference[1].map(r => this.templateName(r)).join(', ')}${NEWLINE}`
158+
}
159+
}
160+
161+
return result
162+
}
163+
}

packages/lg/src/commands/lg/expand.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Command, flags, CLIError} from '@microsoft/bf-cli-command'
1010
import {Helper} from '../../utils'
11-
import {TemplateParser, Templates, DiagnosticSeverity, Diagnostic} from 'botbuilder-lg'
11+
import {TemplatesParser, Templates, DiagnosticSeverity, Diagnostic} from 'botbuilder-lg'
1212
import * as txtfile from 'read-text-file'
1313
import * as path from 'path'
1414
import * as fs from 'fs-extra'
@@ -180,15 +180,19 @@ export default class ExpandCommand extends Command {
180180

181181
const newContent = `#${this.TempTemplateName} \r\n - ${inlineStr}`
182182

183-
return TemplateParser.parseTextWithRef(newContent, lgFile)
183+
return TemplatesParser.parseTextWithRef(newContent, lgFile)
184184
}
185185

186186
private generateExpandedTemplatesFile(expandedTemplates: Map<string, string[]>): string {
187187
let result = ''
188188
for (const template of expandedTemplates) {
189189
result += '# ' + template[0] + '\n'
190190
if (Array.isArray(template[1])) {
191-
for (const templateStr of template[1]) {
191+
for (let templateStr of template[1]) {
192+
if (typeof templateStr !== 'string') {
193+
templateStr = JSON.stringify(templateStr)
194+
}
195+
192196
if (templateStr.includes('\n')) {
193197
// multiline
194198
result += '-```\n' + templateStr.trim() + '\n```\n'

packages/lg/src/commands/lg/verify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default class VerifyCommand extends Command {
1616

1717
static flags: flags.Input<any> = {
1818
in: flags.string({char: 'i', description: 'Folder that contains .lg file.', required: true}),
19-
recurse: flags.boolean({char: 'r', description: 'Considere sub-folders to find .lg file(s)'}),
19+
recurse: flags.boolean({char: 'r', description: 'Considers sub-folders to find .lg file(s)'}),
2020
out: flags.string({char: 'o', description: 'Output file or folder name. If not specified stdout will be used as output'}),
2121
force: flags.boolean({char: 'f', description: 'If --out flag is provided with the path to an existing file, overwrites that file'}),
2222
help: flags.help({char: 'h', description: 'lg:verify help'}),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @module @microsoft/bf-lg-cli
3+
*/
4+
/**
5+
* Copyright (c) Microsoft Corporation. All rights reserved.
6+
* Licensed under the MIT License.
7+
*/
8+
import {test} from '@oclif/test'
9+
import * as fs from 'fs-extra'
10+
import * as path from 'path'
11+
import {TestUtil} from './test-util'
12+
13+
const testcaseFolderPath = './../../fixtures/testcase'
14+
const generatedFolderPath = './../../fixtures/generated'
15+
const verifiedFolderPath = './../../fixtures/verified'
16+
const generatedFolder = path.join(__dirname, generatedFolderPath)
17+
18+
describe('lg:analyze lg file', async () => {
19+
after(async function () {
20+
await fs.remove(generatedFolder)
21+
})
22+
23+
before(async function () {
24+
await fs.remove(generatedFolder)
25+
await fs.mkdirp(generatedFolder)
26+
})
27+
28+
// lg file
29+
test
30+
.command(['lg:analyze',
31+
'--in',
32+
path.join(__dirname, testcaseFolderPath, 'analyze/stop.lg'),
33+
'--out',
34+
generatedFolder,
35+
'-r',
36+
'-f'])
37+
.it('', async () => {
38+
await TestUtil.compareFiles(path.join(generatedFolderPath, 'analysisResult.txt'), path.join(verifiedFolderPath, 'analysisResult1.txt'))
39+
})
40+
41+
// lg files folder
42+
test
43+
.command(['lg:analyze',
44+
'--in',
45+
path.join(__dirname, testcaseFolderPath, 'analyze'),
46+
'--out',
47+
generatedFolder,
48+
'-r',
49+
'-f'])
50+
.it('', async () => {
51+
await TestUtil.compareFiles(path.join(generatedFolderPath, 'analysisResult.txt'), path.join(verifiedFolderPath, 'analysisResult2.txt'))
52+
})
53+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[import](stop.lg)
2+
3+
# greeting
4+
- Hello ${welcome()}
5+
6+
# welcome
7+
- welcome to the new campus
8+
9+
# cancel
10+
- ${stop()}
11+
12+
# teminate
13+
- ${stop()}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# stop
2+
- stop that task
3+
- stop that
4+
- stop the task
5+
6+
# abort
7+
- ${stop()}

0 commit comments

Comments
 (0)