diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md index 079880c66..3ce580d40 100644 --- a/server/CHANGELOG.md +++ b/server/CHANGELOG.md @@ -1,5 +1,14 @@ # Bash Language Server +## 4.4.0 + +- Improve source command parser and include diagnostics when parser fails https://github.com/bash-lsp/bash-language-server/pull/673 +- Fix `onHover` bug where sourced symbols on the same line as a reference would hide documentation https://github.com/bash-lsp/bash-language-server/pull/673 + +## 4.3.2 + +- Improved CLI output https://github.com/bash-lsp/bash-language-server/pull/672 + ## 4.3.0 - Add centralized and configurable logger that can be controlled using the `BASH_IDE_LOG_LEVEL` environment variable and workspace configuration. https://github.com/bash-lsp/bash-language-server/pull/669 diff --git a/server/package.json b/server/package.json index 2e8a43184..1c1e2212f 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,7 @@ "description": "A language server for Bash", "author": "Mads Hartmann", "license": "MIT", - "version": "4.3.2", + "version": "4.4.0", "main": "./out/server.js", "typings": "./out/server.d.ts", "bin": { diff --git a/server/src/__tests__/__snapshots__/analyzer.test.ts.snap b/server/src/__tests__/__snapshots__/analyzer.test.ts.snap index c838b1df1..a4c7e0702 100644 --- a/server/src/__tests__/__snapshots__/analyzer.test.ts.snap +++ b/server/src/__tests__/__snapshots__/analyzer.test.ts.snap @@ -1,43 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`analyze returns a list of errors for a file with a missing node 1`] = ` -Array [ - Object { - "message": "Syntax error: expected \\"fi\\" somewhere in the file", - "range": Object { - "end": Object { - "character": 0, - "line": 12, - }, - "start": Object { - "character": 0, - "line": 12, - }, - }, - "severity": 2, - }, -] -`; - -exports[`analyze returns a list of errors for a file with parsing errors 1`] = ` -Array [ - Object { - "message": "Failed to parse", - "range": Object { - "end": Object { - "character": 1, - "line": 9, - }, - "start": Object { - "character": 0, - "line": 2, - }, - }, - "severity": 1, - }, -] -`; - exports[`findReferences returns a list of locations if parameter is found 1`] = ` Array [ Object { diff --git a/server/src/__tests__/analyzer.test.ts b/server/src/__tests__/analyzer.test.ts index 11de3e8a8..089d24010 100644 --- a/server/src/__tests__/analyzer.test.ts +++ b/server/src/__tests__/analyzer.test.ts @@ -36,30 +36,110 @@ beforeAll(async () => { }) describe('analyze', () => { - it('returns an empty list of errors for a file with no parsing errors', () => { - const result = analyzer.analyze({ + it('returns an empty list of diagnostics for a file with no parsing errors', () => { + const diagnostics = analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL, }) - expect(result).toEqual([]) + expect(diagnostics).toEqual([]) }) - it('returns a list of errors for a file with a missing node', () => { - const result = analyzer.analyze({ + it('returns a list of diagnostics for a file with a missing node', () => { + const diagnostics = analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.MISSING_NODE, }) - expect(result).not.toEqual([]) - expect(result).toMatchSnapshot() + expect(diagnostics).not.toEqual([]) + expect(diagnostics).toMatchInlineSnapshot(` + Array [ + Object { + "message": "Syntax error: expected \\"fi\\" somewhere in the file", + "range": Object { + "end": Object { + "character": 0, + "line": 12, + }, + "start": Object { + "character": 0, + "line": 12, + }, + }, + "severity": 2, + "source": "bash-language-server", + }, + ] + `) }) - it('returns a list of errors for a file with parsing errors', () => { - const result = analyzer.analyze({ + it('returns a list of diagnostics for a file with parsing errors', () => { + const diagnostics = analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.PARSE_PROBLEMS, }) - expect(result).not.toEqual([]) - expect(result).toMatchSnapshot() + expect(diagnostics).not.toEqual([]) + expect(diagnostics).toMatchInlineSnapshot(` + Array [ + Object { + "message": "Failed to parse", + "range": Object { + "end": Object { + "character": 1, + "line": 9, + }, + "start": Object { + "character": 0, + "line": 2, + }, + }, + "severity": 1, + "source": "bash-language-server", + }, + ] + `) + }) + + it('returns a list of diagnostics for a file with sourcing issues', async () => { + const parser = await initializeParser() + const newAnalyzer = new Analyzer({ + parser, + workspaceFolder: FIXTURE_FOLDER, + }) + const diagnostics = newAnalyzer.analyze({ + uri: CURRENT_URI, + document: FIXTURE_DOCUMENT.SOURCING, + }) + expect(diagnostics).not.toEqual([]) + expect(diagnostics).toMatchInlineSnapshot(` + Array [ + Object { + "message": "Source command could not be analyzed: expansion not supported. + + Note that enabling the configuration flag \\"includeAllWorkspaceSymbols\\" + would include all symbols in the workspace regardless of source commands. + But be aware that this will lead to false positive suggestions.", + "range": Object { + "end": Object { + "character": 16, + "line": 21, + }, + "start": Object { + "character": 2, + "line": 21, + }, + }, + "severity": 3, + "source": "bash-language-server", + }, + ] + `) + + // unless setIncludeAllWorkspaceSymbols set + newAnalyzer.setIncludeAllWorkspaceSymbols(true) + const diagnostics2 = newAnalyzer.analyze({ + uri: CURRENT_URI, + document: FIXTURE_DOCUMENT.SOURCING, + }) + expect(diagnostics2).toEqual([]) }) }) diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index 51c5e6075..dcac9dc70 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -208,6 +208,23 @@ describe('server', () => { }, "name": "BOLD", }, + Object { + "kind": 12, + "location": Object { + "range": Object { + "end": Object { + "character": 1, + "line": 22, + }, + "start": Object { + "character": 0, + "line": 20, + }, + }, + "uri": "file://${FIXTURE_FOLDER}sourcing.sh", + }, + "name": "loadlib", + }, ] `) }) diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 9e0948904..7f4b8d925 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -17,6 +17,7 @@ import { } from './util/declarations' import { getFilePaths } from './util/fs' import { logger } from './util/logger' +import { isPositionIncludedInRange } from './util/lsp' import { analyzeShebang } from './util/shebang' import * as sourcing from './util/sourcing' import * as TreeSitterUtil from './util/tree-sitter' @@ -27,6 +28,7 @@ type AnalyzedDocument = { document: TextDocument globalDeclarations: GlobalDeclarations sourcedUris: Set + sourceCommands: sourcing.SourceCommand[] tree: Parser.Tree } @@ -74,18 +76,47 @@ export default class Analyzer { const { diagnostics, globalDeclarations } = getGlobalDeclarations({ tree, uri }) + const sourceCommands = sourcing.getSourceCommands({ + fileUri: uri, + rootPath: this.workspaceFolder, + tree, + }) + + const sourcedUris = new Set( + sourceCommands + .map((sourceCommand) => sourceCommand.uri) + .filter((uri): uri is string => uri !== null), + ) + this.uriToAnalyzedDocument[uri] = { document, globalDeclarations, - sourcedUris: sourcing.getSourcedUris({ - fileContent, - fileUri: uri, - rootPath: this.workspaceFolder, - tree, - }), + sourcedUris, + sourceCommands: sourceCommands.filter((sourceCommand) => !sourceCommand.error), tree, } + if (!this.includeAllWorkspaceSymbols) { + sourceCommands + .filter((sourceCommand) => sourceCommand.error) + .forEach((sourceCommand) => { + diagnostics.push( + LSP.Diagnostic.create( + sourceCommand.range, + [ + `Source command could not be analyzed: ${sourceCommand.error}.\n`, + 'Note that enabling the configuration flag "includeAllWorkspaceSymbols"', + 'would include all symbols in the workspace regardless of source commands.', + 'But be aware that this will lead to false positive suggestions.', + ].join('\n'), + LSP.DiagnosticSeverity.Information, + undefined, + 'bash-language-server', + ), + ) + }) + } + function findMissingNodes(node: Parser.SyntaxNode) { if (node.isMissing()) { diagnostics.push( @@ -93,6 +124,8 @@ export default class Analyzer { TreeSitterUtil.range(node), `Syntax error: expected "${node.type}" somewhere in the file`, LSP.DiagnosticSeverity.Warning, + undefined, + 'bash-language-server', ), ) } else if (node.hasError()) { @@ -198,20 +231,13 @@ export default class Analyzer { uri: string word: string }): LSP.Location[] { - const tree = this.uriToAnalyzedDocument[uri]?.tree + // If the word is sourced, return the location of the source file + const sourcedUri = this.uriToAnalyzedDocument[uri]?.sourceCommands + .filter((sourceCommand) => isPositionIncludedInRange(position, sourceCommand.range)) + .map((sourceCommand) => sourceCommand.uri)[0] - if (position && tree) { - // NOTE: when a word is a file path to a sourced file, we return a location to it. - const sourcedLocation = sourcing.getSourcedLocation({ - position, - rootPath: this.workspaceFolder, - tree, - uri, - word, - }) - if (sourcedLocation) { - return [sourcedLocation] - } + if (sourcedUri) { + return [LSP.Location.create(sourcedUri, LSP.Range.create(0, 0, 0, 0))] } return this.findDeclarationsMatchingWord({ diff --git a/server/src/config.ts b/server/src/config.ts index 692d429c9..3eac34e92 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -61,7 +61,8 @@ export function getConfigFromEnvironmentVariables(): { const environmentVariablesUsed = Object.entries(rawConfig) .map(([key, value]) => (typeof value !== 'undefined' ? key : null)) - .filter((key) => key !== null) as string[] + .filter((key): key is string => key !== null) + .filter((key) => key !== 'logLevel') // logLevel is a special case that we ignore const config = ConfigSchema.parse(rawConfig) diff --git a/server/src/server.ts b/server/src/server.ts index 832ec6984..22a4a1726 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -364,6 +364,9 @@ export default class BashServer { symbol: LSP.SymbolInformation currentUri: string }): LSP.MarkupContent { + logger.debug( + `getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`, + ) const symbolUri = symbol.location.uri const symbolStartLine = symbol.location.range.start.line @@ -665,6 +668,9 @@ export default class BashServer { Builtins.isBuiltin(word) || (this.executables.isExecutableOnPATH(word) && symbolsMatchingWord.length == 0) ) { + logger.debug( + `onHover: getting shell documentation for reserved word or builtin or executable`, + ) const shellDocumentation = await getShellDocumentation({ word }) if (shellDocumentation) { return { contents: getMarkdownContent(shellDocumentation, 'man') } @@ -675,7 +681,11 @@ export default class BashServer { currentUri, }) // do not return hover referencing for the current line - .filter((symbol) => symbol.location.range.start.line !== params.position.line) + .filter( + (symbol) => + symbol.location.uri !== currentUri || + symbol.location.range.start.line !== params.position.line, + ) .map((symbol: LSP.SymbolInformation) => this.getDocumentationForSymbol({ currentUri, symbol }), ) diff --git a/server/src/util/__tests__/__snapshots__/sourcing.test.ts.snap b/server/src/util/__tests__/__snapshots__/sourcing.test.ts.snap new file mode 100644 index 000000000..b6f343d1e --- /dev/null +++ b/server/src/util/__tests__/__snapshots__/sourcing.test.ts.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getSourcedUris returns a set of sourced files 2`] = ` +Array [ + Object { + "error": null, + "range": Object { + "end": Object { + "character": 28, + "line": 1, + }, + "start": Object { + "character": 6, + "line": 1, + }, + }, + "uri": "file:///Users/bash/file-in-path.sh", + }, + Object { + "error": null, + "range": Object { + "end": Object { + "character": 31, + "line": 3, + }, + "start": Object { + "character": 6, + "line": 3, + }, + }, + "uri": "file:///bin/extension.inc", + }, + Object { + "error": null, + "range": Object { + "end": Object { + "character": 22, + "line": 5, + }, + "start": Object { + "character": 6, + "line": 5, + }, + }, + "uri": "file:///Users/bash/x", + }, + Object { + "error": null, + "range": Object { + "end": Object { + "character": 36, + "line": 7, + }, + "start": Object { + "character": 6, + "line": 7, + }, + }, + "uri": "file:///Users/scripts/release-client.sh", + }, + Object { + "error": null, + "range": Object { + "end": Object { + "character": 23, + "line": 9, + }, + "start": Object { + "character": 6, + "line": 9, + }, + }, + "uri": "file:///Users/bash-user/myscript", + }, + Object { + "error": "expansion not supported", + "range": Object { + "end": Object { + "character": 23, + "line": 13, + }, + "start": Object { + "character": 6, + "line": 13, + }, + }, + "uri": null, + }, + Object { + "error": "expansion not supported", + "range": Object { + "end": Object { + "character": 66, + "line": 16, + }, + "start": Object { + "character": 49, + "line": 16, + }, + }, + "uri": null, + }, + Object { + "error": "expansion not supported", + "range": Object { + "end": Object { + "character": 66, + "line": 18, + }, + "start": Object { + "character": 6, + "line": 18, + }, + }, + "uri": null, + }, + Object { + "error": null, + "range": Object { + "end": Object { + "character": 30, + "line": 25, + }, + "start": Object { + "character": 8, + "line": 25, + }, + }, + "uri": "file:///Users/bash/issue206.sh", + }, + Object { + "error": "expansion not supported", + "range": Object { + "end": Object { + "character": 22, + "line": 43, + }, + "start": Object { + "character": 8, + "line": 43, + }, + }, + "uri": null, + }, +] +`; diff --git a/server/src/util/__tests__/sourcing.test.ts b/server/src/util/__tests__/sourcing.test.ts index 4d2e62fa8..4c9b7d5ab 100644 --- a/server/src/util/__tests__/sourcing.test.ts +++ b/server/src/util/__tests__/sourcing.test.ts @@ -3,7 +3,7 @@ import * as os from 'os' import * as Parser from 'web-tree-sitter' import { initializeParser } from '../../parser' -import { getSourcedUris } from '../sourcing' +import { getSourceCommands } from '../sourcing' const fileDirectory = '/Users/bash' const fileUri = `${fileDirectory}/file.sh` @@ -21,13 +21,12 @@ jest.spyOn(os, 'homedir').mockImplementation(() => '/Users/bash-user') describe('getSourcedUris', () => { it('returns an empty set if no files were sourced', () => { const fileContent = '' - const result = getSourcedUris({ - fileContent, + const sourceCommands = getSourceCommands({ fileUri, rootPath: null, tree: parser.parse(fileContent), }) - expect(result).toEqual(new Set([])) + expect(sourceCommands).toEqual([]) }) it('returns a set of sourced files', () => { @@ -44,18 +43,20 @@ describe('getSourcedUris', () => { # source ... - source "./issue206.sh" # quoted file in fixtures folder - source "$LIBPATH" # dynamic imports not supported # conditional is currently not supported if [[ -z $__COMPLETION_LIB_LOADED ]]; then source "$LIBPATH" ; fi + . "$BASH_IT/themes/$BASH_IT_THEME/$BASH_IT_THEME.theme.bash" + show () { about 'Shows SVN proxy settings' group 'proxy' + source "./issue206.sh" # quoted file in fixtures folder + echo "SVN Proxy Settings" echo "==================" python2 - < { fi done + loadlib () { + source "$1.sh" + } + + loadlib "issue101" ` - const result = getSourcedUris({ - fileContent, + const sourceCommands = getSourceCommands({ fileUri, rootPath: null, tree: parser.parse(fileContent), }) - expect(result).toMatchInlineSnapshot(` + const sourcedUris = new Set( + sourceCommands + .map((sourceCommand) => sourceCommand.uri) + .filter((uri) => uri !== null), + ) + + expect(sourcedUris).toMatchInlineSnapshot(` Set { "file:///Users/bash/file-in-path.sh", "file:///bin/extension.inc", @@ -90,5 +101,7 @@ describe('getSourcedUris', () => { "file:///Users/bash/issue206.sh", } `) + + expect(sourceCommands).toMatchSnapshot() }) }) diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index b288aabd3..09c04e0a2 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -46,6 +46,8 @@ export function getGlobalDeclarations({ TreeSitterUtil.range(node), 'Failed to parse', LSP.DiagnosticSeverity.Error, + undefined, + 'bash-language-server', ), ) return @@ -77,7 +79,7 @@ export function getAllDeclarationsInTree({ }): LSP.SymbolInformation[] { const symbols: LSP.SymbolInformation[] = [] - TreeSitterUtil.forEach(tree.rootNode, (node: Parser.SyntaxNode) => { + TreeSitterUtil.forEach(tree.rootNode, (node) => { if (TreeSitterUtil.isDefinition(node)) { const symbol = nodeToSymbolInformation({ node, uri }) @@ -189,7 +191,7 @@ function getAllGlobalVariableDeclarations({ }) { const declarations: Declarations = {} - TreeSitterUtil.forEach(rootNode, (node: Parser.SyntaxNode) => { + TreeSitterUtil.forEach(rootNode, (node) => { if ( node.type === 'variable_assignment' && // exclude local variables diff --git a/server/src/util/lsp.ts b/server/src/util/lsp.ts new file mode 100644 index 000000000..a335c206c --- /dev/null +++ b/server/src/util/lsp.ts @@ -0,0 +1,13 @@ +import * as LSP from 'vscode-languageserver/node' + +/** + * Determine if a position is included in a range. + */ +export function isPositionIncludedInRange(position: LSP.Position, range: LSP.Range) { + return ( + range.start.line <= position.line && + range.end.line >= position.line && + range.start.character <= position.character && + range.end.character >= position.character + ) +} diff --git a/server/src/util/sourcing.ts b/server/src/util/sourcing.ts index c342e4849..d5e9e2038 100644 --- a/server/src/util/sourcing.ts +++ b/server/src/util/sourcing.ts @@ -4,118 +4,108 @@ import * as LSP from 'vscode-languageserver' import * as Parser from 'web-tree-sitter' import { untildify } from './fs' +import { logger } from './logger' +import * as TreeSitterUtil from './tree-sitter' -// Until the grammar supports sourcing, we use this little regular expression -const SOURCING_STATEMENTS = /^(?:\t|[ ])*(?:source|[.])\s*(\S*)/ const SOURCING_COMMANDS = ['source', '.'] +export type SourceCommand = { + range: LSP.Range + uri: string | null // resolved URIs + error: string | null +} + /** - * Analysis the given file content and returns a set of URIs that are - * sourced. Note that the URIs are resolved. + * Analysis the given tree for source commands. */ -export function getSourcedUris({ - fileContent, +export function getSourceCommands({ fileUri, rootPath, tree, }: { - fileContent: string fileUri: string rootPath: string | null tree: Parser.Tree -}): Set { - const uris: Set = new Set([]) +}): SourceCommand[] { + const sourceCommands: SourceCommand[] = [] + const rootPaths = [path.dirname(fileUri), rootPath].filter(Boolean) as string[] - fileContent.split(/\r?\n/).forEach((line, lineIndex) => { - const match = line.match(SOURCING_STATEMENTS) - if (match) { - const [statement, word] = match - - if (tree.rootNode) { - const node = tree.rootNode.descendantForPosition({ - row: lineIndex, - column: statement.length - 2, - }) - if (['heredoc_body', 'raw_string'].includes(node?.type)) { - return - } - } + TreeSitterUtil.forEach(tree.rootNode, (node) => { + const sourcedPathInfo = getSourcedPathInfoFromNode({ node }) - const sourcedUri = getSourcedUri({ rootPaths, word }) - if (sourcedUri) { - uris.add(sourcedUri) - } + if (sourcedPathInfo) { + const { sourcedPath, parseError } = sourcedPathInfo + const uri = sourcedPath ? resolveSourcedUri({ rootPaths, sourcedPath }) : null + + sourceCommands.push({ + range: TreeSitterUtil.range(node), + uri, + error: uri ? null : parseError || 'failed to resolve path', + }) } + + return true }) - return uris + return sourceCommands } -/** - * Investigates if the given position is a path to a sourced file and maps it - * to a location. Useful for jump to definition. - * - * TODO: we could likely store the position as part of the getSourcedUris and - * get rid of this function. - * - * @returns an optional location - */ -export function getSourcedLocation({ - position, - rootPath, - tree, - uri, - word, +function getSourcedPathInfoFromNode({ + node, }: { - position: { line: number; character: number } - rootPath: string | null - tree: Parser.Tree - uri: string - word: string -}): LSP.Location | null { - // NOTE: when a word is a file path to a sourced file, we return a location to - // that file. - if (tree.rootNode) { - const node = tree.rootNode.descendantForPosition({ - row: position.line, - column: position.character, - }) - - if (!node || node.text.trim() !== word) { - throw new Error('Implementation error: word was not found at the given position') - } - - const isSourced = node.previousNamedSibling - ? SOURCING_COMMANDS.includes(node.previousNamedSibling.text.trim()) - : false + node: Parser.SyntaxNode +}): null | { sourcedPath?: string; parseError?: string } { + if (node.type === 'command') { + const [commandNameNode, argumentNode] = node.namedChildren + if ( + commandNameNode.type === 'command_name' && + SOURCING_COMMANDS.includes(commandNameNode.text) + ) { + if (argumentNode.type === 'word') { + return { + sourcedPath: argumentNode.text, + } + } - const rootPaths = [path.dirname(uri), rootPath].filter(Boolean) as string[] + if (argumentNode.type === 'simple_expansion') { + return { + parseError: 'expansion not supported', + } + } - const sourcedUri = isSourced ? getSourcedUri({ word, rootPaths }) : null + if (argumentNode.type === 'string') { + if (argumentNode.namedChildren.length === 0) { + return { + sourcedPath: argumentNode.text.slice(1, -1), + } + } else if ( + argumentNode.namedChildren.every((n) => n.type === 'simple_expansion') + ) { + return { + parseError: 'expansion not supported', + } + } else { + logger.warn( + 'Sourcing: unhandled argumentNode=string case', + argumentNode.namedChildren.map((c) => ({ type: c.type, text: c.text })), + ) + } + } else { + logger.warn(`Sourcing: unhandled argumentNode=${argumentNode.type} case`) + } - if (sourcedUri) { - return LSP.Location.create(sourcedUri, LSP.Range.create(0, 0, 0, 0)) + return { + parseError: `unhandled case node type "${argumentNode.type}"`, + } } } return null } -const stripQuotes = (path: string): string => { - const first = path[0] - const last = path[path.length - 1] - - if (first === last && [`"`, `'`].includes(first)) { - return path.slice(1, -1) - } - - return path -} - /** - * Tries to parse the given path and returns a URI if possible. - * - Filters out dynamic sources + * Tries to resolve the given sourced path and returns a URI if possible. * - Converts a relative paths to absolute paths * - Converts a tilde path to an absolute path * - Resolves the path @@ -124,34 +114,27 @@ const stripQuotes = (path: string): string => { * "If filename does not contain a slash, file names in PATH are used to find * the directory containing filename." (see https://ss64.com/osx/source.html) */ -function getSourcedUri({ +function resolveSourcedUri({ rootPaths, - word, + sourcedPath, }: { rootPaths: string[] - word: string + sourcedPath: string }): string | null { - let unquotedPath = stripQuotes(word) - - if (unquotedPath.includes('$')) { - // NOTE: we don't support dynamic sourcing - return null - } - - if (unquotedPath.startsWith('~')) { - unquotedPath = untildify(unquotedPath) + if (sourcedPath.startsWith('~')) { + sourcedPath = untildify(sourcedPath) } - if (unquotedPath.startsWith('/')) { - if (fs.existsSync(unquotedPath)) { - return `file://${unquotedPath}` + if (sourcedPath.startsWith('/')) { + if (fs.existsSync(sourcedPath)) { + return `file://${sourcedPath}` } return null } // resolve relative path for (const rootPath of rootPaths) { - const potentialPath = path.join(rootPath.replace('file://', ''), unquotedPath) + const potentialPath = path.join(rootPath.replace('file://', ''), sourcedPath) // check if path is a file if (fs.existsSync(potentialPath)) { diff --git a/testing/fixtures/sourcing.sh b/testing/fixtures/sourcing.sh index 04c58ca3a..35227490f 100644 --- a/testing/fixtures/sourcing.sh +++ b/testing/fixtures/sourcing.sh @@ -17,3 +17,9 @@ echo "$" source ./scripts/tag-release.inc tagRelease '1.0.0' + +loadlib () { + source "$1.sh" +} + +loadlib "issue206"