Skip to content

Commit

Permalink
feat: added support for go ecosystem (#210)
Browse files Browse the repository at this point in the history
Signed-off-by: Ilona Shishov <Ilona.Shishov@gmail.com>
  • Loading branch information
IlonaShishov authored Sep 19, 2023
1 parent c972cdc commit 8c25d44
Showing 23 changed files with 195 additions and 363 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: Stage
name: Release

on:
workflow_dispatch:
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Red Hat Dependency Analytics LSP Server

![Release](https://github.com/fabric8-analytics/fabric8-analytics-lsp-server/workflows/Release/badge.svg?branch=master)
[![NPM Version](https://img.shields.io/npm/v/fabric8-analytics-lsp-server.svg)](https://www.npmjs.com/package/fabric8-analytics-lsp-server)
![CI Build](https://github.com/fabric8-analytics/fabric8-analytics-lsp-server/workflows/CI%20Build/badge.svg?branch=master)
[![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)
![GitHub Package Version](https://img.shields.io/github/package-json/v/fabric8-analytics/fabric8-analytics-lsp-server/master?logo=github&label=GitHub%20Package)
![CI](https://github.com/fabric8-analytics/fabric8-analytics-lsp-server/workflows/CI/badge.svg?branch=master)
[![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)

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

## Build

@@ -21,9 +21,14 @@ npm test

## Release

Semantic release are done via Github Actions using `semantic-release`.
- merging each PR will result with an automatic build of master
- and a release apatch, minor or major version. You should use correct [commit message](https://github.com/semantic-release/semantic-release#commit-message-format).
Releases are done via Github Actions using `npm` to publish packages to `GitHub Packages`.
- Upon merging a pull request into the master branch, an automated staging process for the master branch is initiated.
- The version specified in the package.json file is adjusted in accordance with the predefined configuration, which may involve bumping it to a prerelease, patch, minor, or major version.
- Subsequently, the project undergoes the building process.
- A package is published to GitHub Packages, containing the latest modifications.
- A commit is made, signifying the changes introduced in the package.
- A tag is created, reflecting the updated version number.
- Finally, a release is issued, accompanied by the appropriate version identification.

## Clients

28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@
"dist"
],
"dependencies": {
"@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-alpha.4",
"@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.27",
"@xml-tools/ast": "^5.0.5",
"@xml-tools/parser": "^1.0.11",
"compare-versions": "^6.0.0-rc.1",
13 changes: 11 additions & 2 deletions src/aggregators.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
* Licensed under the Apache-2.0 License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
'use strict';
import { IDependencyProvider } from './collector';
import { Vulnerability } from './vulnerability';

/* VulnerabilityAggregator */
@@ -14,11 +15,15 @@ interface VulnerabilityAggregator {
/* Noop Vulnerability aggregator class */
class NoopVulnerabilityAggregator implements VulnerabilityAggregator {
isNewVulnerability: boolean;
provider: IDependencyProvider;
constructor(provider: IDependencyProvider) {
this.provider = provider;
}

aggregate(newVulnerability: Vulnerability): Vulnerability {
// Make it a new vulnerability always and set ecosystem for vulnerability.
this.isNewVulnerability = true;
newVulnerability.ecosystem = newVulnerability.ref.split(':')[1].split('/')[0];
newVulnerability.provider = this.provider;

return newVulnerability;
}
@@ -28,6 +33,10 @@ class NoopVulnerabilityAggregator implements VulnerabilityAggregator {
class MavenVulnerabilityAggregator implements VulnerabilityAggregator {
isNewVulnerability: boolean;
vulnerabilities: Map<string, Vulnerability> = new Map<string, Vulnerability>();
provider: IDependencyProvider;
constructor(provider: IDependencyProvider) {
this.provider = provider;
}

aggregate(newVulnerability: Vulnerability): Vulnerability {
// Make it a new vulnerability always and set ecosystem for vulnerability.
@@ -38,7 +47,7 @@ class MavenVulnerabilityAggregator implements VulnerabilityAggregator {
this.isNewVulnerability = false;
return v;
}
newVulnerability.ecosystem = 'maven';
newVulnerability.provider = this.provider;
this.vulnerabilities.set(key, newVulnerability);
return newVulnerability;
}
7 changes: 4 additions & 3 deletions src/collector.ts
Original file line number Diff line number Diff line change
@@ -84,8 +84,9 @@ export interface IHashableDependency extends IDependency {
key(): string;
}

/* Dependency collector interface */
export interface IDependencyCollector {
/* Ecosystem provider interface */
export interface IDependencyProvider {
ecosystem: string;
classes: Array<string>;
collect(contents: string): Promise<Array<IDependency>>;
}
@@ -113,7 +114,7 @@ export class Dependency implements IHashableDependency {
}

key(): string {
return `${this.name.value}`;
return `${this.name.value}${/^[~^]/.test(this.version.value) ? '' : `@${this.version.value}`}`;
}
}

2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ class Config
utm_source: string;
mvn_executable: string;
npm_executable: string;
go_executable: string;
exhort_dev_mode: string;

constructor() {
@@ -27,6 +28,7 @@ class Config
this.utm_source = process.env.UTM_SOURCE || '';
this.mvn_executable = process.env.MVN_EXECUTABLE || 'mvn';
this.npm_executable = process.env.NPM_EXECUTABLE || 'npm';
this.go_executable = process.env.GO_EXECUTABLE || 'go';
this.exhort_dev_mode = process.env.EXHORT_DEV_MODE || 'false';
}
}
4 changes: 2 additions & 2 deletions src/consumers.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,10 @@
* ------------------------------------------------------------------------------------------ */
'use strict';
import { IDependency } from './collector';
import { get_range, VERSION_TEMPLATE } from './utils';
import { get_range } from './utils';
import { Vulnerability } from './vulnerability';
import { VulnerabilityAggregator } from './aggregators';
import { Diagnostic, CodeAction, CodeActionKind } from 'vscode-languageserver';
import { Diagnostic, CodeAction } from 'vscode-languageserver';

/* Descriptor describing what key-path to extract from the document */
interface IBindingDescriptor {
66 changes: 13 additions & 53 deletions src/collector/go.mod.ts → src/providers/go.mod.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
'use strict';

import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyCollector, Dependency } from '../collector';
import { config } from '../config';
import { exec } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector';

/* Please note :: There was issue with semverRegex usage in the code. During run time, it extracts
* version with 'v' prefix, but this is not be behavior of semver in CLI and test environment.
@@ -16,8 +12,8 @@ function semVerRegExp(line: string): RegExpExecArray {
}

class NaiveGomodParser {
constructor(contents: string, goImports: Set<string>) {
this.dependencies = NaiveGomodParser.parseDependencies(contents, goImports);
constructor(contents: string) {
this.dependencies = NaiveGomodParser.parseDependencies(contents);
}

dependencies: Array<IDependency>;
@@ -54,21 +50,22 @@ class NaiveGomodParser {
return replaceDependency;
}

static parseDependencies(contents:string, goImports: Set<string>): Array<IDependency> {
static parseDependencies(contents:string): Array<IDependency> {
let replaceMap = new Map<string, IDependency>();
let goModDeps = contents.split('\n').reduce((dependencies, line, index) => {

// skip any text after '//'
if (line.includes('//')) {
line = line.split('//')[0];
}

if (line.includes('=>')) {
let replaceEntry = NaiveGomodParser.getReplaceMap(line, index);
if (replaceEntry) {
replaceMap.set(replaceEntry.key, replaceEntry.value);
}
} else {
// Not using semver directly, look at comment on import statement.
//const version = semverRegex().exec(line)
const version = semVerRegExp(line);
// Skip lines without version string
if (version && version.length > 0) {
@@ -87,63 +84,26 @@ class NaiveGomodParser {
return dependencies;
}, []);

let goPackageDeps = [];

goImports.forEach(importStatement => {
let exactMatchDep: Dependency = null;
let moduleMatchDep: Dependency = null;
goModDeps.forEach(goModDep => {
if (importStatement === goModDep.name.value) {
// Software stack uses the module
exactMatchDep = goModDep;
} else if (importStatement.startsWith(goModDep.name.value + '/')) {
// Find longest module name that matches the import statement
if (moduleMatchDep === null) {
moduleMatchDep = goModDep;
} else if (moduleMatchDep.name.value.length < goModDep.name.value.length) {
moduleMatchDep = goModDep;
}
}
});

if (exactMatchDep === null && moduleMatchDep !== null) {
// Software stack uses a package from the module
let replaceDependency = NaiveGomodParser.applyReplaceMap(moduleMatchDep, replaceMap);
if (replaceDependency !== moduleMatchDep) {
importStatement = importStatement.replace(moduleMatchDep.name.value, replaceDependency.name.value);
}
const entry: IKeyValueEntry = new KeyValueEntry(importStatement + '@' + replaceDependency.name.value, replaceDependency.name.position);
entry.value = new Variant(ValueType.String, replaceDependency.version.value);
entry.value_position = replaceDependency.version.position;
goPackageDeps.push(new Dependency(entry));
}
});

goModDeps = goModDeps.map(goModDep => NaiveGomodParser.applyReplaceMap(goModDep, replaceMap));

// Return modules present in go.mod and packages used in imports.
return [...goModDeps, ...goPackageDeps];
return [...goModDeps];
}

parse(): Array<IDependency> {
return this.dependencies;
}
}

/* Process entries found in the go.mod file and collect all dependency
* related information */
export class DependencyCollector implements IDependencyCollector {
constructor(private manifestFile: string, public classes: Array<string> = ['dependencies']) {
this.manifestFile = manifestFile;
/* Process entries found in the go.mod file */
export class DependencyProvider implements IDependencyProvider {
ecosystem: string;
constructor(public classes: Array<string> = ['dependencies']) {
this.ecosystem = 'golang';
}

async collect(contents: string): Promise<Array<IDependency>> {
let promiseExec = new Promise<Set<string>>((resolve, reject) => {
reject('Golang is not a supported packae manager');
});
const goImports: Set<string> = await promiseExec;
let parser = new NaiveGomodParser(contents, goImports);
let parser = new NaiveGomodParser(contents);
return parser.parse();
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use strict';

import jsonAst from 'json-to-ast';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyCollector, Dependency } from '../collector';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector';

export class DependencyCollector implements IDependencyCollector {
constructor(public classes: Array<string> = ['dependencies']) {}
export class DependencyProvider implements IDependencyProvider {
ecosystem: string;
constructor(public classes: Array<string> = ['dependencies']) {
this.ecosystem = 'npm';
}

async collect(contents: string): Promise<Array<IDependency>> {
let ast: any;
10 changes: 6 additions & 4 deletions src/collector/pom.xml.ts → src/providers/pom.xml.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use strict';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyCollector, Dependency } from '../collector';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector';
import { parse, DocumentCstNode } from '@xml-tools/parser';
import { buildAst, accept, XMLElement, XMLDocument } from '@xml-tools/ast';
import { VERSION_TEMPLATE } from '../utils';

export class DependencyCollector implements IDependencyCollector {
export class DependencyProvider implements IDependencyProvider {
private xmlDocAst: XMLDocument;
private originalDeps: Array<XMLElement>;
ecosystem: string;

constructor(originalContents: string, enforceVersions: boolean, public classes: Array<string> = ['dependencies']) {
this.ecosystem = 'maven';
const { cst, tokenVector } = parse(originalContents);
const originalXmlDocAst = buildAst(cst as DocumentCstNode, tokenVector);
if (originalXmlDocAst.rootElement) {
@@ -62,7 +64,7 @@ export class DependencyCollector implements IDependencyCollector {

const toDependency = (resolved: PomDependency, original: PomDependency): Dependency => {
const dep: IKeyValueEntry = new KeyValueEntry(
`${original.groupId.textContents[0].text}:${original.artifactId.textContents[0].text}`,
`${original.groupId.textContents[0].text}/${original.artifactId.textContents[0].text}`,
{ line: original.element.position.startLine, column: original.element.position.startColumn }
);
dep.context_range = {
@@ -101,7 +103,7 @@ export class DependencyCollector implements IDependencyCollector {
};

const getMapKey = (element: PomDependency): string => {
return `${element.groupId.textContents[0].text}:${element.artifactId.textContents[0].text}`;
return `${element.groupId.textContents[0].text}/${element.artifactId.textContents[0].text}`;
};

const buildDependencyMap = (original: Array<PomDependency>, resolved: Array<PomDependency>): Array<PomDependency> => {
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyCollector, Dependency } from '../collector';
import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector';

class NaivePyParser {
constructor(contents: string) {
@@ -37,12 +37,14 @@ class NaivePyParser {

/* Process entries found in the txt files and collect all dependency
* related information */
export class DependencyCollector implements IDependencyCollector {
constructor(public classes: Array<string> = ['dependencies']) {}
export class DependencyProvider implements IDependencyProvider {
ecosystem: string;
constructor(public classes: Array<string> = ['dependencies']) {
this.ecosystem = 'python';
}

async collect(contents: string): Promise<Array<IDependency>> {
let parser = new NaivePyParser(contents);
return parser.parse();
}

}
Loading

0 comments on commit 8c25d44

Please sign in to comment.