diff --git a/server/package.json b/server/package.json index 88ed86ae5..dfaddfee5 100644 --- a/server/package.json +++ b/server/package.json @@ -17,7 +17,7 @@ "node": "*" }, "dependencies": { - "glob": "^7.1.2", + "glob": "^7.1.3", "request": "^2.83.0", "request-promise-native": "^1.0.5", "tree-sitter": "^0.13.22", @@ -30,5 +30,8 @@ "compile": "rm -rf out && ../node_modules/.bin/tsc -p ./", "compile:watch": "../node_modules/.bin/tsc -w -p ./", "prepublishOnly": "yarn run compile" + }, + "devDependencies": { + "@types/glob": "^7.1.1" } } diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 70ff69a71..e6961e887 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -1,7 +1,6 @@ // tslint:disable:no-submodule-imports import * as fs from 'fs' import * as glob from 'glob' -import * as Path from 'path' import * as request from 'request-promise-native' import * as Parser from 'tree-sitter' @@ -12,6 +11,8 @@ import * as LSP from 'vscode-languageserver' import { uniqueBasedOnHash } from './util/array' import { flattenArray, flattenObjectValues } from './util/flatten' import * as TreeSitterUtil from './util/tree-sitter' +import { hasBashShebang } from './util/shebang'; +import { getGlobPattern } from './config'; type Kinds = { [type: string]: LSP.SymbolKind } @@ -33,41 +34,55 @@ export default class Analyzer { * If the rootPath is provided it will initialize all *.sh files it can find * anywhere on that path. */ - public static fromRoot( + public static async fromRoot( connection: LSP.Connection, rootPath: string | null, ): Promise { - // This happens if the users opens a single bash script without having the - // 'window' associated with a specific project. - if (!rootPath) { - return Promise.resolve(new Analyzer()) + const analyzer = new Analyzer() + + if (rootPath) { + const lookupStartTime = Date.now() + + const globPattern = getGlobPattern() + connection.console.log(`Looking up files matching "${globPattern}"`) + + const filePaths = await this.getFilePaths({globPattern, rootPath}) + + filePaths.forEach(filePath => { + const fileContent = fs.readFileSync(filePath, 'utf8') + if (!hasBashShebang(fileContent)) { + connection.console.log(`No bash shebang found for ${filePath}`) + return + } + + connection.console.log(`Analyzing ${filePath}`) + + const uri = 'file://' + filePath + analyzer.analyze( + uri, + LSP.TextDocument.create( + uri, + 'shell', + 1, + fileContent, + ), + ) + }) + + connection.console.log(`Analyzing finished after ${(Date.now() - lookupStartTime)/1000} seconds`) } + return analyzer + } + + private static getFilePaths({globPattern, rootPath}:{ globPattern: string, rootPath: string}): Promise { return new Promise((resolve, reject) => { - glob('**/*.sh', { cwd: rootPath }, (err, paths) => { - if (err != null) { - reject(err) - } else { - const analyzer = new Analyzer() - paths.forEach(p => { - const absolute = Path.join(rootPath, p) - // only analyze files, glob pattern may match directories - if (fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) { - const uri = 'file://' + absolute - connection.console.log('Analyzing ' + uri) - analyzer.analyze( - uri, - LSP.TextDocument.create( - uri, - 'shell', - 1, - fs.readFileSync(absolute, 'utf8'), - ), - ) - } - }) - resolve(analyzer) + glob(globPattern, { cwd: rootPath, nodir: true, absolute: true }, function (err, files) { + if (err) { + return reject(err) } + + resolve(files) }) }) } diff --git a/server/src/config.ts b/server/src/config.ts index 719553a51..e9f427b55 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -3,6 +3,11 @@ export function getExplainshellEndpoint(): string | null { return typeof EXPLAINSHELL_ENDPOINT !== 'undefined' ? EXPLAINSHELL_ENDPOINT : null } +export function getGlobPattern(): string { + const { GLOB_PATTERN } = process.env + return typeof GLOB_PATTERN === 'string' ? GLOB_PATTERN : '**/*@(.sh|.inc|.bash|.command)' +} + export function getHighlightParsingError(): boolean { const { HIGHLIGHT_PARSING_ERRORS } = process.env return typeof HIGHLIGHT_PARSING_ERRORS !== 'undefined' diff --git a/server/src/util/__tests__/shebang.test.ts b/server/src/util/__tests__/shebang.test.ts new file mode 100644 index 000000000..8c1e021bd --- /dev/null +++ b/server/src/util/__tests__/shebang.test.ts @@ -0,0 +1,31 @@ +import {hasBashShebang} from '../shebang' + +describe('hasBashShebang', () => { + it('returns false for empty file', () => { + expect(hasBashShebang('')).toBe(false) + }) + + it('returns false for python files', () => { + expect(hasBashShebang(`#!/usr/bin/env python2.7\n# set -x`)).toBe(false) + }) + + it('returns true for "#!/bin/sh -"', () => { + expect(hasBashShebang('#!/bin/sh -')).toBe(true) + expect(hasBashShebang('#!/bin/sh - ')).toBe(true) + }) + + it('returns true for "#!/usr/bin/env bash"', () => { + expect(hasBashShebang('#!/usr/bin/env bash')).toBe(true) + expect(hasBashShebang('#!/usr/bin/env bash ')).toBe(true) + }) + + it('returns true for "#!/bin/sh"', () => { + expect(hasBashShebang('#!/bin/sh')).toBe(true) + expect(hasBashShebang('#!/bin/sh ')).toBe(true) + }) + + it('returns true for "#!/bin/bash"', () => { + expect(hasBashShebang('#!/bin/bash')).toBe(true) + expect(hasBashShebang('#!/bin/bash ')).toBe(true) + }) +}) diff --git a/server/src/util/shebang.ts b/server/src/util/shebang.ts new file mode 100644 index 000000000..cb6479cac --- /dev/null +++ b/server/src/util/shebang.ts @@ -0,0 +1,11 @@ +const SHEBANG_REGEXP = /^#!(.*)/ + +export function hasBashShebang(fileContent: string) { + const match = SHEBANG_REGEXP.exec(fileContent) + if (!match || !match[1]) { + return false + } + + const shebang = match[1].replace('-', '').trim() + return shebang.endsWith('bash') || shebang.endsWith('sh') +} diff --git a/server/yarn.lock b/server/yarn.lock index b89feae71..437d7febc 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -2,6 +2,30 @@ # yarn lockfile v1 +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*": + version "11.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" + integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== + abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -373,10 +397,10 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== +glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" diff --git a/testing/fixtures/include.inc b/testing/fixtures/include.inc new file mode 100644 index 000000000..e8f9172e3 --- /dev/null +++ b/testing/fixtures/include.inc @@ -0,0 +1,4 @@ +#!/bin/sh + +export XXX='my export' + diff --git a/testing/fixtures/included.sh b/testing/fixtures/included.sh new file mode 100644 index 000000000..aa745d9ad --- /dev/null +++ b/testing/fixtures/included.sh @@ -0,0 +1,5 @@ +#!/bin/sh +. $(dirname "$0")/include.inc + +echo ${XXX} + diff --git a/testing/fixtures/no-extension b/testing/fixtures/no-extension new file mode 100644 index 000000000..b39c374af --- /dev/null +++ b/testing/fixtures/no-extension @@ -0,0 +1,8 @@ +#!/bin/bash +configures="`env | grep 'npm_config_' | sed -e 's|^npm_config_||g'`" + +ret=$? +if [ $ret -ne 0 ]; the + echo "It failed" >&2 +fi +exit $ret diff --git a/testing/fixtures/not-shell.sh b/testing/fixtures/not-shell.sh new file mode 100644 index 000000000..d76b97283 --- /dev/null +++ b/testing/fixtures/not-shell.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env python2.7 +# set -x + +def func(): + print 'hello world'