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

Commit db68ce1

Browse files
authored
fix: Add property tag support for pom.xml (#172)
- Use @xml-tools/ast for pom.xml parsing (AST based, previous one is based on SAX parsing) - Refactor pom collector into separate module
1 parent fac0a87 commit db68ce1

File tree

9 files changed

+586
-282
lines changed

9 files changed

+586
-282
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![NPM Publish](https://ci.centos.org/job/devtools-fabric8-analytics-lsp-server-npm-publish-build-master/badge/icon)](https://ci.centos.org/job/devtools-fabric8-analytics-lsp-server-npm-publish-build-master/)
44
[![NPM Version](https://img.shields.io/npm/v/fabric8-analytics-lsp-server.svg)](https://www.npmjs.com/package/fabric8-analytics-lsp-server)
5-
![CI Build](https://github.com/fabric8-analytics/fabric8-analytics-lsp-server/workflows/CI%20Build/badge.svg)
5+
![CI Build](https://github.com/fabric8-analytics/fabric8-analytics-lsp-server/workflows/CI%20Build/badge.svg?branch=master)
66
[![codecov](https://codecov.io/gh/fabric8-analytics/fabric8-analytics-lsp-server/branch/master/graph/badge.svg?token=aVThXjheDf)](https://codecov.io/gh/fabric8-analytics/fabric8-analytics-lsp-server)
77

88
Language Server(LSP) that can analyze your dependencies specified in `package.json` and `pom.xml`.

package-lock.json

Lines changed: 278 additions & 127 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@
3030
"url": "https://github.com/fabric8-analytics/fabric8-analytics-lsp-server.git"
3131
},
3232
"dependencies": {
33+
"@xml-tools/ast": "^5.0.0",
34+
"@xml-tools/parser": "^1.0.7",
35+
"compare-versions": "3.6.0",
3336
"json-to-ast": "^2.1.0",
3437
"node-fetch": "^2.6.0",
3538
"vscode-languageserver": "^5.3.0-next.9",
36-
"winston": "3.2.1",
37-
"xml2object": "0.1.2",
38-
"compare-versions": "3.6.0"
39+
"winston": "3.2.1"
3940
},
4041
"devDependencies": {
4142
"@semantic-release/exec": "^5.0.0",

src/collector.ts

Lines changed: 4 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
* ------------------------------------------------------------------------------------------ */
55
'use strict';
66
import { Stream } from 'stream';
7-
import * as Xml2Object from 'xml2object';
87
import * as jsonAst from 'json-to-ast';
9-
import { IPosition, IKeyValueEntry, KeyValueEntry, Variant, ValueType } from './types';
8+
import { IPosition, IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IPositionedString, IDependencyCollector, Dependency } from './types';
109
import { stream_from_string, getGoLangImportsCmd } from './utils';
1110
import { config } from './config';
1211
import { exec } from 'child_process';
12+
import { parse, DocumentCstNode } from "@xml-tools/parser";
13+
import { buildAst, accept, XMLElement, XMLDocument } from "@xml-tools/ast";
1314

1415
/* Please note :: There was issue with semverRegex usage in the code. During run time, it extracts
1516
* version with 'v' prefix, but this is not be behavior of semver in CLI and test environment.
@@ -20,40 +21,6 @@ function semVerRegExp(line: string): RegExpExecArray {
2021
return regExp.exec(line);
2122
}
2223

23-
/* String value with position */
24-
interface IPositionedString {
25-
value: string;
26-
position: IPosition;
27-
}
28-
29-
/* Dependency specification */
30-
interface IDependency {
31-
name: IPositionedString;
32-
version: IPositionedString;
33-
}
34-
35-
/* Dependency collector interface */
36-
interface IDependencyCollector {
37-
classes: Array<string>;
38-
collect(contents: string): Promise<Array<IDependency>>;
39-
}
40-
41-
/* Dependency class that can be created from `IKeyValueEntry` */
42-
class Dependency implements IDependency {
43-
name: IPositionedString;
44-
version: IPositionedString;
45-
constructor(dependency: IKeyValueEntry) {
46-
this.name = {
47-
value: dependency.key,
48-
position: dependency.key_position
49-
};
50-
this.version = {
51-
value: dependency.value.object,
52-
position: dependency.value_position
53-
};
54-
}
55-
}
56-
5724
class NaivePyParser {
5825
constructor(contents: string) {
5926
this.dependencies = NaivePyParser.parseDependencies(contents);
@@ -245,87 +212,6 @@ class GomodDependencyCollector implements IDependencyCollector {
245212

246213
}
247214

248-
class NaivePomXmlSaxParser {
249-
constructor(stream: Stream) {
250-
this.stream = stream;
251-
this.parser = this.createParser();
252-
}
253-
254-
stream: Stream;
255-
parser: Xml2Object;
256-
dependencies: Array<IDependency> = [];
257-
isDependency: boolean = false;
258-
versionStartLine: number = 0;
259-
versionStartColumn: number = 0;
260-
261-
createParser(): Xml2Object {
262-
let parser = new Xml2Object([ "dependency" ], {strict: true, trackPosition: true});
263-
let deps = this.dependencies;
264-
let versionLine = this.versionStartLine;
265-
let versionColumn = this.versionStartColumn;
266-
267-
parser.on("object", function (name, obj) {
268-
if (obj.hasOwnProperty("groupId") && obj.hasOwnProperty("artifactId") && obj.hasOwnProperty("version") &&
269-
(!obj.hasOwnProperty("scope") || (obj.hasOwnProperty("scope") && obj["scope"] != "test"))) {
270-
let ga = `${obj["groupId"]}:${obj["artifactId"]}`;
271-
let entry: IKeyValueEntry = new KeyValueEntry(ga, {line: 0, column: 0});
272-
entry.value = new Variant(ValueType.String, obj["version"]);
273-
entry.value_position = {line: versionLine, column: versionColumn};
274-
let dep: IDependency = new Dependency(entry);
275-
deps.push(dep)
276-
}
277-
});
278-
parser.saxStream.on("opentag", function (node) {
279-
if (node.name == "dependency") {
280-
this.isDependency = true;
281-
}
282-
if (this.isDependency && node.name == "version") {
283-
versionLine = parser.saxStream._parser.line + 1;
284-
versionColumn = parser.saxStream._parser.column +1;
285-
}
286-
});
287-
parser.saxStream.on("closetag", function (nodeName) {
288-
// TODO: nested deps!
289-
if (nodeName == "dependency") {
290-
this.isDependency = false;
291-
}
292-
});
293-
parser.on("error", function (e) {
294-
// the XML document doesn't have to be well-formed, that's fine
295-
parser.error = null;
296-
});
297-
parser.on("end", function () {
298-
// the XML document doesn't have to be well-formed, that's fine
299-
// parser.error = null;
300-
this.dependencies = deps;
301-
});
302-
return parser
303-
}
304-
305-
async parse() {
306-
return new Promise(resolve => {
307-
this.stream.pipe(this.parser.saxStream).on('end', (data) => {
308-
resolve(this.dependencies);
309-
});
310-
});
311-
312-
}
313-
}
314-
315-
class PomXmlDependencyCollector implements IDependencyCollector {
316-
constructor(public classes: Array<string> = ["dependencies"]) {}
317-
318-
async collect(contents: string): Promise<Array<IDependency>> {
319-
const file = stream_from_string(contents);
320-
let parser = new NaivePomXmlSaxParser(file);
321-
let dependencies;
322-
await parser.parse().then(data => {
323-
dependencies = data;
324-
});
325-
return dependencies || [];
326-
}
327-
}
328-
329215
class PackageJsonCollector implements IDependencyCollector {
330216
constructor(public classes: Array<string> = ["dependencies"]) {}
331217

@@ -343,4 +229,4 @@ class PackageJsonCollector implements IDependencyCollector {
343229
}
344230
}
345231

346-
export { IDependencyCollector, PackageJsonCollector, PomXmlDependencyCollector, ReqDependencyCollector, GomodDependencyCollector, IPositionedString, IDependency };
232+
export { IDependencyCollector, PackageJsonCollector, ReqDependencyCollector, GomodDependencyCollector, IPositionedString, IDependency };

src/maven.collector.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict';
2+
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyCollector, Dependency, IPositionedString, IPosition } from './types';
3+
import { parse, DocumentCstNode } from "@xml-tools/parser";
4+
import { buildAst, accept, XMLElement, XMLDocument } from "@xml-tools/ast";
5+
6+
export class PomXmlDependencyCollector implements IDependencyCollector {
7+
private xmlDocAst: XMLDocument;
8+
9+
constructor(public classes: Array<string> = ["dependencies"]) {}
10+
11+
private findRootNodes(rootElementName: string): Array<XMLElement> {
12+
const properties: Array<XMLElement> = [];
13+
const propertiesElement = {
14+
// Will be invoked once for each Element node in the AST.
15+
visitXMLElement: (node: XMLElement) => {
16+
if (node.name === rootElementName) {
17+
properties.push(node);
18+
}
19+
},
20+
};
21+
accept(this.xmlDocAst, propertiesElement);
22+
return properties;
23+
}
24+
25+
private parseXml(contents: string): void {
26+
const { cst, tokenVector } = parse(contents);
27+
this.xmlDocAst = buildAst(cst as DocumentCstNode, tokenVector);
28+
}
29+
30+
private mapToDependency(dependenciesNode: XMLElement): Array<IDependency> {
31+
class PomDependency {
32+
public groupId: XMLElement;
33+
public artifactId: XMLElement;
34+
public version: XMLElement;
35+
constructor(e: XMLElement) {
36+
this.groupId = e.subElements.find(e => e.name === 'groupId');
37+
this.artifactId = e.subElements.find(e => e.name === 'artifactId');
38+
this.version = e.subElements.find(e => e.name === 'version');
39+
}
40+
41+
isValid(): boolean {
42+
// none should have a empty text.
43+
return [this.groupId, this.artifactId, this.version].find(e => !e.textContents[0]?.text) === undefined;
44+
}
45+
46+
toDependency(): Dependency {
47+
const dep: IKeyValueEntry = new KeyValueEntry(`${this.groupId.textContents[0].text}:${this.artifactId.textContents[0].text}`, {line: 0, column: 0});
48+
const versionVal = this.version.textContents[0];
49+
dep.value = new Variant(ValueType.String, versionVal.text);
50+
dep.value_position = {line: versionVal.position.startLine, column: versionVal.position.startColumn};
51+
return new Dependency(dep);
52+
}
53+
};
54+
const validElementNames = ['groupId', 'artifactId', 'version'];
55+
const dependencies = dependenciesNode?.
56+
subElements.
57+
filter(e => e.name === 'dependency').
58+
// must include all validElementNames
59+
filter(e => e.subElements.filter(e => validElementNames.includes(e.name)).length == validElementNames.length).
60+
// no test dependencies
61+
filter(e => !e.subElements.find(e => (e.name === 'scope' && e.textContents[0].text === 'test'))).
62+
map(e => new PomDependency(e)).
63+
filter(d => d.isValid()).
64+
map(d => d.toDependency());
65+
return dependencies || [];
66+
}
67+
68+
private createPropertySubstitution(e: XMLElement): Map<string, IPositionedString> {
69+
return new Map(e?.subElements?.
70+
filter(e => e.textContents[0]?.text).
71+
map(e => {
72+
const propertyValue = e.textContents[0];
73+
const position: IPosition = {line: propertyValue.position.startLine, column: propertyValue.position.startColumn};
74+
const value = {value: propertyValue.text, position: position} as IPositionedString;
75+
// key should be equivalent to pom.xml property format. i.e ${property.value}
76+
return [`\$\{${e.name}\}`, value];
77+
}));
78+
}
79+
80+
private applyProperty(dependency: IDependency, map: Map<string, IPositionedString>): IDependency {
81+
// FIXME: Do the groupId and artifactId will also be expressed through properties?
82+
dependency.version = map.get(dependency.version.value) ?? dependency.version;
83+
return dependency;
84+
}
85+
86+
async collect(contents: string): Promise<Array<IDependency>> {
87+
this.parseXml(contents);
88+
const deps = this.findRootNodes("dependencies");
89+
// lazy eval
90+
const getPropertyMap = (() => {
91+
let propertyMap = null;
92+
return () => {
93+
propertyMap = propertyMap ?? this.createPropertySubstitution(this.findRootNodes("properties")[0]);
94+
return propertyMap;
95+
};
96+
})();
97+
98+
return deps.flatMap(dep => this.mapToDependency(dep)).map(d => this.applyProperty(d, getPropertyMap()));
99+
}
100+
}

src/server.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import * as fs from 'fs';
88
import {
99
IPCMessageReader, IPCMessageWriter, createConnection, IConnection,
1010
TextDocuments, InitializeResult, CodeLens, CodeAction, CodeActionKind} from 'vscode-languageserver';
11-
import { IDependencyCollector, PackageJsonCollector, PomXmlDependencyCollector, ReqDependencyCollector, GomodDependencyCollector } from './collector';
11+
import { IDependencyCollector, PackageJsonCollector, ReqDependencyCollector, GomodDependencyCollector } from './collector';
12+
import { PomXmlDependencyCollector } from './maven.collector';
1213
import { SecurityEngine, DiagnosticsPipeline, codeActionsMap } from './consumers';
1314
import { NoopVulnerabilityAggregator, GolangVulnerabilityAggregator } from './aggregators';
1415
import { AnalyticsSource } from './vulnerability';
@@ -250,11 +251,14 @@ const sendDiagnostics = async (ecosystem: string, diagnosticFilePath: string, co
250251
connection.sendNotification('caNotification', {data: caDefaultMsg, done: false, uri: diagnosticFilePath});
251252
let deps = null;
252253
try {
254+
const start = new Date().getTime();
253255
deps = await collector.collect(contents);
256+
const end = new Date().getTime();
257+
connection.console.log(`manifest parse took ${end - start} ms`);
254258
} catch (error) {
255259
// Error can be raised during golang `go list ` command only.
256260
if (ecosystem == "golang") {
257-
connection.console.error(`Command execution failed with error: ${error}`);
261+
connection.console.warn(`Command execution failed with error: ${error}`);
258262
connection.sendNotification('caError', {data: error, uri: diagnosticFilePath});
259263
connection.sendDiagnostics({ uri: diagnosticFilePath, diagnostics: [] });
260264
return;
@@ -282,7 +286,7 @@ const sendDiagnostics = async (ecosystem: string, diagnosticFilePath: string, co
282286
await Promise.allSettled(allRequests);
283287
const end = new Date().getTime();
284288

285-
connection.console.log('Time taken to fetch vulnerabilities: ' + ((end - start) / 1000).toFixed(1) + ' sec.');
289+
connection.console.log(`fetch vulns took ${end - start} ms`);
286290
connection.sendNotification('caNotification', {data: getCAmsg(deps, diagnostics, totalCount), diagCount : diagnostics.length || 0, vulnCount: totalCount, depCount: deps.length || 0, done: true, uri: diagnosticFilePath});
287291
};
288292

src/types.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,38 @@ class Variant implements IVariant {
5353
constructor(public type: ValueType, public object: any) {}
5454
}
5555

56+
/* String value with position */
57+
interface IPositionedString {
58+
value: string;
59+
position: IPosition;
60+
}
61+
62+
/* Dependency specification */
63+
interface IDependency {
64+
name: IPositionedString;
65+
version: IPositionedString;
66+
}
5667

57-
export { IPosition, IKeyValueEntry, KeyValueEntry, Variant, ValueType };
68+
/* Dependency collector interface */
69+
interface IDependencyCollector {
70+
classes: Array<string>;
71+
collect(contents: string): Promise<Array<IDependency>>;
72+
}
73+
74+
/* Dependency class that can be created from `IKeyValueEntry` */
75+
class Dependency implements IDependency {
76+
name: IPositionedString;
77+
version: IPositionedString;
78+
constructor(dependency: IKeyValueEntry) {
79+
this.name = {
80+
value: dependency.key,
81+
position: dependency.key_position
82+
};
83+
this.version = {
84+
value: dependency.value.object,
85+
position: dependency.value_position
86+
};
87+
}
88+
}
5889

90+
export { IPosition, IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IPositionedString, IDependencyCollector, Dependency };

0 commit comments

Comments
 (0)