Merge pull request #187 from github/joshmgross/lint-language-service

Lint the language service package
This commit is contained in:
Josh Gross
2023-03-16 14:31:24 -04:00
committed by GitHub
21 changed files with 62 additions and 55 deletions
+9 -9
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {MarkupContent, TextEdit} from "vscode-languageserver-types"; import {MarkupContent, TextEdit} from "vscode-languageserver-types";
import {complete} from "./complete"; import {complete} from "./complete";
import {WorkflowContext} from "./context/workflow-context";
import {registerLogger} from "./log"; import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position"; import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger"; import {TestLogger} from "./test-utils/logger";
@@ -182,8 +182,8 @@ jobs:
const config: ValueProviderConfig = { const config: ValueProviderConfig = {
"runs-on": { "runs-on": {
kind: ValueProviderKind.SuggestedValues, kind: ValueProviderKind.SuggestedValues,
get: async (_: WorkflowContext) => { get: () => {
return [{label: "my-custom-label"}]; return Promise.resolve([{label: "my-custom-label"}]);
} }
} }
}; };
@@ -200,8 +200,8 @@ jobs:
const config: ValueProviderConfig = { const config: ValueProviderConfig = {
"runs-on": { "runs-on": {
kind: ValueProviderKind.SuggestedValues, kind: ValueProviderKind.SuggestedValues,
get: async (_: WorkflowContext) => { get: () => {
return [{label: "my-custom-label"}]; return Promise.resolve([{label: "my-custom-label"}]);
} }
} }
}; };
@@ -349,7 +349,7 @@ jobs:
`; `;
const result = await complete(...getPositionFromCursor(input)); const result = await complete(...getPositionFromCursor(input));
expect(result).toHaveLength(16); expect(result).toHaveLength(16);
let textEdit = result[0].textEdit as TextEdit; const textEdit = result[0].textEdit as TextEdit;
expect(textEdit.range).toEqual({ expect(textEdit.range).toEqual({
start: {line: 5, character: 4}, start: {line: 5, character: 4},
end: {line: 5, character: 5} end: {line: 5, character: 5}
@@ -390,7 +390,7 @@ jobs:
expect(result).not.toBeUndefined(); expect(result).not.toBeUndefined();
expect(result.length).toEqual(1); expect(result.length).toEqual(1);
let textEdit = result[0].textEdit as TextEdit; const textEdit = result[0].textEdit as TextEdit;
expect(textEdit.newText).toEqual("pre-build"); expect(textEdit.newText).toEqual("pre-build");
expect(textEdit.range).toEqual({ expect(textEdit.range).toEqual({
start: {line: 6, character: 11}, start: {line: 6, character: 11},
@@ -405,7 +405,7 @@ jobs:
expect(result).not.toBeUndefined(); expect(result).not.toBeUndefined();
expect(result.map(e => e.label)).toContain("runs-on"); expect(result.map(e => e.label)).toContain("runs-on");
let textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit; const textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
expect(textEdit.newText).toEqual("runs-on"); expect(textEdit.newText).toEqual("runs-on");
expect(textEdit.range).toEqual({ expect(textEdit.range).toEqual({
start: {line: 3, character: 4}, start: {line: 3, character: 4},
@@ -420,7 +420,7 @@ jobs:
expect(result).not.toBeUndefined(); expect(result).not.toBeUndefined();
expect(result.map(e => e.label)).toContain("runs-on"); expect(result.map(e => e.label)).toContain("runs-on");
let textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit; const textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
expect(textEdit.newText).toEqual("runs-on"); expect(textEdit.newText).toEqual("runs-on");
expect(textEdit.range).toEqual({ expect(textEdit.range).toEqual({
start: {line: 3, character: 4}, start: {line: 3, character: 4},
@@ -10,5 +10,6 @@ export const RootContext = "root";
*/ */
export function getDescription(context: string, key: string): string | undefined { export function getDescription(context: string, key: string): string | undefined {
// The inferred type doesn't quite match the actual type, use any to work around that // The inferred type doesn't quite match the actual type, use any to work around that
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
return (descriptions as any)[context]?.[key]?.description; return (descriptions as any)[context]?.[key]?.description;
} }
@@ -18,7 +18,7 @@ export function getJobsContext(workflowContext: WorkflowContext): DescriptionDic
const outputs = job.outputs || new data.Null(); const outputs = job.outputs || new data.Null();
if (outputs instanceof MappingToken) { if (outputs instanceof MappingToken) {
jobContext.add("outputs", createOutputsContext(outputs as MappingToken), getDescription("jobs", "outputs")); jobContext.add("outputs", createOutputsContext(outputs), getDescription("jobs", "outputs"));
} }
jobsContext.add(job.id.toString(), jobContext); jobsContext.add(job.id.toString(), jobContext);
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {data, DescriptionDictionary} from "@github/actions-expressions"; import {data, DescriptionDictionary} from "@github/actions-expressions";
import {Job} from "@github/actions-workflow-parser/model/workflow-template"; import {Job} from "@github/actions-workflow-parser/model/workflow-template";
import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token"; import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token";
@@ -121,7 +121,7 @@ function matrixProperties(matrix: MappingToken, mode: Mode): Map<string, Set<str
case "exclude": case "exclude":
break; break;
default: default: {
if (!isSequence(pair.value)) { if (!isSequence(pair.value)) {
properties.set(key, undefined); properties.set(key, undefined);
continue; continue;
@@ -139,6 +139,7 @@ function matrixProperties(matrix: MappingToken, mode: Mode): Map<string, Set<str
break; break;
} }
} }
}
if (include) { if (include) {
for (const item of include) { for (const item of include) {
@@ -4,7 +4,11 @@ import {WorkflowContext} from "../context/workflow-context";
import {TokenResult} from "../utils/find-token"; import {TokenResult} from "../utils/find-token";
export function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean { export function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean {
return tokenResult.parent?.definition?.key === "workflow-job-with" && isString(tokenResult.token!); return (
tokenResult.parent?.definition?.key === "workflow-job-with" &&
tokenResult.token !== null &&
isString(tokenResult.token)
);
} }
export function getReusableWorkflowInputDescription( export function getReusableWorkflowInputDescription(
@@ -29,7 +29,7 @@ export function mapToExpressionPos(token: TemplateToken, position: Position): Ex
if (token.originalExpressions?.length) { if (token.originalExpressions?.length) {
for (const originalExp of token.originalExpressions) { for (const originalExp of token.originalExpressions) {
// Find the original expression that contains the position // Find the original expression that contains the position
if (posWithinRange(pos, originalExp.expressionRange!)) { if (originalExp.expressionRange && posWithinRange(pos, originalExp.expressionRange)) {
const exprRange = mapRange(originalExp.expressionRange); const exprRange = mapRange(originalExp.expressionRange);
return { return {
@@ -47,7 +47,7 @@ export function mapToExpressionPos(token: TemplateToken, position: Position): Ex
return undefined; return undefined;
} }
const exprRange = mapRange(token.expressionRange!); const exprRange = mapRange(token.expressionRange);
return { return {
expression: token.expression, expression: token.expression,
// Adjust the position to point into the expression // Adjust the position to point into the expression
@@ -11,10 +11,11 @@ import {getPositionFromCursor} from "../test-utils/cursor-position";
import {HoverVisitor} from "./visitor"; import {HoverVisitor} from "./visitor";
const contextProviderConfig: ContextProviderConfig = { const contextProviderConfig: ContextProviderConfig = {
getContext: async (context: string) => { getContext: (context: string) => {
switch (context) { switch (context) {
case "github": case "github":
return new DescriptionDictionary( return Promise.resolve(
new DescriptionDictionary(
{ {
key: "event", key: "event",
value: new data.StringData("push"), value: new data.StringData("push"),
@@ -29,10 +30,11 @@ const contextProviderConfig: ContextProviderConfig = {
}), }),
description: "Test dictionary" description: "Test dictionary"
} }
)
); );
} }
return undefined; return Promise.resolve(undefined);
} }
}; };
@@ -128,7 +130,6 @@ async function hoverExpression(input: string) {
column: pos.character column: pos.character
}, },
context, context,
[],
validatorFunctions validatorFunctions
); );
return hv.hover(expr); return hv.hover(expr);
@@ -16,7 +16,7 @@ import {
Logical, Logical,
Unary Unary
} from "@github/actions-expressions/ast"; } from "@github/actions-expressions/ast";
import {FunctionDefinition, FunctionInfo} from "@github/actions-expressions/funcs/info"; import {FunctionDefinition} from "@github/actions-expressions/funcs/info";
import {Pos, Range} from "@github/actions-expressions/lexer"; import {Pos, Range} from "@github/actions-expressions/lexer";
import {posWithinRange} from "./pos-range"; import {posWithinRange} from "./pos-range";
@@ -35,7 +35,6 @@ export class HoverVisitor implements ExprVisitor<HoverResult> {
constructor( constructor(
private pos: Pos, private pos: Pos,
private context: DescriptionDictionary, private context: DescriptionDictionary,
private extensionFunctions: FunctionInfo[],
private functions: Map<string, FunctionDefinition> private functions: Map<string, FunctionDefinition>
) {} ) {}
@@ -43,7 +42,7 @@ export class HoverVisitor implements ExprVisitor<HoverResult> {
return n.accept(this); return n.accept(this);
} }
visitLiteral(literal: Literal): HoverResult { visitLiteral(): HoverResult {
return undefined; return undefined;
} }
@@ -12,7 +12,7 @@ export class ErrorDictionary extends data.Dictionary {
constructor(...pairs: Pair[]) { constructor(...pairs: Pair[]) {
super(...pairs); super(...pairs);
} }
public complete: boolean = true; public complete = true;
get(key: string): ExpressionData | undefined { get(key: string): ExpressionData | undefined {
const value = super.get(key); const value = super.get(key);
+1 -1
View File
@@ -13,7 +13,7 @@ export function testHoverConfig(tokenValue: string, tokenKey: string, descriptio
throw new Error("Test provider only supports string tokens"); throw new Error("Test provider only supports string tokens");
} }
expect((token as StringToken).value).toEqual(tokenValue); expect(token.value).toEqual(tokenValue);
expect(token.definition!.key).toEqual(tokenKey); expect(token.definition!.key).toEqual(tokenKey);
return description; return description;
+2 -2
View File
@@ -112,7 +112,7 @@ function appendContext(description: string, allowedContext?: string[]) {
if (!allowedContext || allowedContext.length == 0) { if (!allowedContext || allowedContext.length == 0) {
return description; return description;
} }
let {namedContexts, functions} = splitAllowedContext(allowedContext); const {namedContexts, functions} = splitAllowedContext(allowedContext);
let namedContextsString = ""; let namedContextsString = "";
let functionsString = ""; let functionsString = "";
@@ -169,7 +169,7 @@ function expressionHover(
const p = new Parser(lr.tokens, namedContexts, functions); const p = new Parser(lr.tokens, namedContexts, functions);
const expr = p.parse(); const expr = p.parse();
const hv = new HoverVisitor(position, context, [], validatorFunctions); const hv = new HoverVisitor(position, context, validatorFunctions);
const hoverResult = hv.hover(expr); const hoverResult = hv.hover(expr);
if (!hoverResult) { if (!hoverResult) {
return null; return null;
@@ -2,6 +2,7 @@ import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provi
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference"; import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
export const testFileProvider: FileProvider = { export const testFileProvider: FileProvider = {
// eslint-disable-next-line @typescript-eslint/require-await
getFileContent: async ref => { getFileContent: async ref => {
switch (fileIdentifier(ref)) { switch (fileIdentifier(ref)) {
case "monalisa/octocat/workflow.yaml@main": case "monalisa/octocat/workflow.yaml@main":
+1 -1
View File
@@ -40,7 +40,7 @@ function testFindToken(input: string): {
parent: getTokenInfo(r.parent), parent: getTokenInfo(r.parent),
key: getTokenInfo(r.keyToken), key: getTokenInfo(r.keyToken),
token: getTokenInfo(r.token), token: getTokenInfo(r.token),
path: r.path.map(x => getTokenInfo(x)!) path: r.path.map(x => getTokenInfo(x)!) // eslint-disable-line @typescript-eslint/no-non-null-assertion
}; };
} }
+5 -4
View File
@@ -52,7 +52,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
]; ];
while (s.length > 0) { while (s.length > 0) {
const result = s.shift()!; const result = s.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
const {parent, token, keyToken, path} = result; const {parent, token, keyToken, path} = result;
if (!token) { if (!token) {
break; break;
@@ -67,7 +67,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
// Position is in token, enqueue children if there are any // Position is in token, enqueue children if there are any
switch (token.templateTokenType) { switch (token.templateTokenType) {
case TokenType.Mapping: case TokenType.Mapping: {
const mappingToken = token as MappingToken; const mappingToken = token as MappingToken;
for (const {key, value} of mappingToken) { for (const {key, value} of mappingToken) {
// If the position is within the key, immediately return it as the token. // If the position is within the key, immediately return it as the token.
@@ -99,8 +99,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
}); });
} }
continue; continue;
}
case TokenType.Sequence: case TokenType.Sequence: {
const sequenceToken = token as SequenceToken; const sequenceToken = token as SequenceToken;
for (const token of sequenceToken) { for (const token of sequenceToken) {
s.push({ s.push({
@@ -112,6 +112,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
} }
continue; continue;
} }
}
return { return {
token, token,
@@ -43,8 +43,8 @@ function getLineCharCode(doc: TextDocument, lineNumber: number, index: number):
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
class SpacesDiffResult { class SpacesDiffResult {
public spacesDiff: number = 0; public spacesDiff = 0;
public looksLikeAlignment: boolean = false; public looksLikeAlignment = false;
} }
/** /**
+2 -1
View File
@@ -39,7 +39,8 @@ export async function fetchOrConvertWorkflowTemplate(
if (!template) { if (!template) {
template = await convertWorkflowTemplate( template = await convertWorkflowTemplate(
parsedWorkflow.context, parsedWorkflow.context,
parsedWorkflow.value!, // TODO: @joshmgross We can't assume that the value is non-null here
parsedWorkflow.value!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
config?.fileProvider, config?.fileProvider,
options options
); );
+1 -1
View File
@@ -64,7 +64,7 @@ export async function validate(textDocument: TextDocument, config?: ValidationCo
// For now map parser errors directly to diagnostics // For now map parser errors directly to diagnostics
for (const error of result.context.errors.getErrors()) { for (const error of result.context.errors.getErrors()) {
let range = mapRange(error.range); const range = mapRange(error.range);
diagnostics.push({ diagnostics.push({
message: error.rawMessage, message: error.rawMessage,
@@ -1,4 +1,3 @@
import {WorkflowContext} from "../context/workflow-context";
import {ValueProviderConfig, ValueProviderKind} from "./config"; import {ValueProviderConfig, ValueProviderKind} from "./config";
import {needs} from "./needs"; import {needs} from "./needs";
import {reusableJobInputs} from "./reusable-job-inputs"; import {reusableJobInputs} from "./reusable-job-inputs";
@@ -23,18 +22,18 @@ export const DEFAULT_RUNNER_LABELS = [
export const defaultValueProviders: ValueProviderConfig = { export const defaultValueProviders: ValueProviderConfig = {
needs: { needs: {
kind: ValueProviderKind.AllowedValues, kind: ValueProviderKind.AllowedValues,
get: needs get: context => Promise.resolve(needs(context))
}, },
"workflow-job-with": { "workflow-job-with": {
kind: ValueProviderKind.AllowedValues, kind: ValueProviderKind.AllowedValues,
get: async context => reusableJobInputs(context) get: context => Promise.resolve(reusableJobInputs(context))
}, },
"workflow-job-secrets": { "workflow-job-secrets": {
kind: ValueProviderKind.SuggestedValues, kind: ValueProviderKind.SuggestedValues,
get: async (context, existingValues) => reusableJobSecrets(context, existingValues) get: (context, existingValues) => Promise.resolve(reusableJobSecrets(context, existingValues))
}, },
"runs-on": { "runs-on": {
kind: ValueProviderKind.SuggestedValues, kind: ValueProviderKind.SuggestedValues,
get: async (_: WorkflowContext) => stringsToValues(DEFAULT_RUNNER_LABELS) get: () => Promise.resolve(stringsToValues(DEFAULT_RUNNER_LABELS))
} }
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
import {WorkflowContext} from "../context/workflow-context"; import {WorkflowContext} from "../context/workflow-context";
import {Value} from "./config"; import {Value} from "./config";
export async function needs(context: WorkflowContext): Promise<Value[]> { export function needs(context: WorkflowContext): Value[] {
if (!context.template) { if (!context.template) {
return []; return [];
} }
@@ -1,9 +1,7 @@
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
import {isMapping, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; import {isMapping, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
import {WorkflowContext} from "../context/workflow-context"; import {WorkflowContext} from "../context/workflow-context";
import {Value} from "./config"; import {Value} from "./config";
import {stringsToValues} from "./strings-to-values";
export function reusableJobInputs(context: WorkflowContext): Value[] { export function reusableJobInputs(context: WorkflowContext): Value[] {
if (!context.reusableWorkflowJob?.["input-definitions"]) { if (!context.reusableWorkflowJob?.["input-definitions"]) {