Skip to content

Commit 4bdd722

Browse files
authored
Merge #3770 from aws/samscan
2 parents 262bd39 + fe432f9 commit 4bdd722

File tree

7 files changed

+134
-27
lines changed

7 files changed

+134
-27
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "SAM template detection now skips directories specified in [user settings](https://code.visualstudio.com/docs/getstarted/settings) `files.exclude`, `search.exclude`, or `files.watcherExclude`. This improves performance on big workspaces and avoids the \"Scanning CloudFormation templates...\" message. [#3510](https://github.com/aws/aws-toolkit-vscode/issues/3510)"
4+
}

src/shared/fs/watchedFiles.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@ import * as vscode from 'vscode'
77
import { getLogger } from '../logger/logger'
88
import * as pathutils from '../utilities/pathUtils'
99
import * as path from 'path'
10-
import { isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils'
10+
import { globDirs, isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils'
11+
import { Settings } from '../settings'
12+
import { once } from '../utilities/functionUtils'
1113

1214
/**
1315
* Prevent `findFiles()` from recursing into these directories.
1416
*
1517
* `findFiles()` defaults to the vscode `files.exclude` setting, which by default does not exclude "node_modules/".
1618
*/
17-
const alwaysExclude = '**/{.aws-sam,.git,.svn,.hg,.rvm,.gem,.project,node_modules,venv,bower_components}/'
19+
const alwaysExclude = {
20+
'.aws-sam': true,
21+
'.git': true,
22+
'.svn': true,
23+
'.hg': true,
24+
'.rvm': true,
25+
'.gem': true,
26+
'.project': true,
27+
node_modules: true,
28+
venv: true,
29+
bower_components: true,
30+
}
1831

1932
export interface WatchedItem<T> {
2033
/**
@@ -27,6 +40,21 @@ export interface WatchedItem<T> {
2740
item: T
2841
}
2942

43+
/** Builds an exclude pattern based on vscode global settings and the `alwaysExclude` default. */
44+
export function getExcludePattern() {
45+
const vscodeFilesExclude = Settings.instance.get<object>('files.exclude', Object, {})
46+
const vscodeSearchExclude = Settings.instance.get<object>('search.exclude', Object, {})
47+
const vscodeWatcherExclude = Settings.instance.get<object>('files.watcherExclude', Object, {})
48+
const all = [
49+
...Object.keys(alwaysExclude),
50+
...Object.keys(vscodeFilesExclude),
51+
...Object.keys(vscodeSearchExclude),
52+
...Object.keys(vscodeWatcherExclude),
53+
]
54+
return globDirs(all)
55+
}
56+
const getExcludePatternOnce = once(getExcludePattern)
57+
3058
/**
3159
* WatchedFiles lets us index files in the current registry. It is used
3260
* for CFN templates among other things. WatchedFiles holds a list of pairs of
@@ -230,10 +258,19 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
230258
*/
231259
public async rebuild(): Promise<void> {
232260
this.reset()
261+
262+
const exclude = getExcludePatternOnce()
233263
for (const glob of this.globs) {
234-
const itemUris = await vscode.workspace.findFiles(glob, alwaysExclude)
235-
for (const item of itemUris) {
236-
await this.addItemToRegistry(item, true)
264+
try {
265+
const found = await vscode.workspace.findFiles(glob, exclude)
266+
for (const item of found) {
267+
await this.addItemToRegistry(item, true)
268+
}
269+
} catch (e) {
270+
const err = e as Error
271+
if (err.name !== 'Canceled') {
272+
getLogger().error('watchedFiles: findFiles("%s", "%s"): %s', glob, exclude, err.message)
273+
}
237274
}
238275
}
239276
}

src/shared/logger/activation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export async function activate(
4848
)
4949

5050
setLogger(mainLogger)
51-
getLogger().error(`log level: ${getLogLevel()}`)
51+
getLogger().info(`log level: ${getLogLevel()}`)
5252

5353
// channel logger
5454
setLogger(

src/shared/settings.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,35 @@ export class Settings {
3131
) {}
3232

3333
/**
34-
* Reads a setting, applying the {@link TypeConstructor} if provided.
34+
* Gets a setting value, applying the {@link TypeConstructor} if provided.
3535
*
36-
* Note that the absence of a key is indistinguisable from the absence of a value.
37-
* That is, `undefined` looks the same across all calls. Any non-existent values are
38-
* simply returned as-is without passing through the type cast.
36+
* If the read fails or the setting value is invalid (does not conform to `type`):
37+
* - `defaultValue` is returned if it was provided
38+
* - else an exception is thrown
39+
*
40+
* @note An unknown setting is indistinguisable from a missing value: both return `undefined`.
41+
* Non-existent values are returned as-is without passing through the type cast.
42+
*
43+
* @param key Setting name
44+
* @param type Expected setting type
45+
* @param defaultValue Value returned if setting is missing or invalid
3946
*/
4047
public get(key: string): unknown
4148
public get<T>(key: string, type: TypeConstructor<T>): T | undefined
4249
public get<T>(key: string, type: TypeConstructor<T>, defaultValue: T): T
4350
public get<T>(key: string, type?: TypeConstructor<T>, defaultValue?: T) {
44-
const value = this.getConfig().get(key, defaultValue)
51+
try {
52+
const value = this.getConfig().get(key, defaultValue)
4553

46-
return !type || value === undefined ? value : cast(value, type)
54+
return !type || value === undefined ? value : cast(value, type)
55+
} catch (e) {
56+
if (arguments.length <= 2) {
57+
throw ToolkitError.chain(e, `Failed to read setting "${key}"`)
58+
}
59+
getLogger().error('settings: failed to read "%s": %s', key, (e as Error).message)
60+
61+
return defaultValue
62+
}
4763
}
4864

4965
/**
@@ -62,8 +78,8 @@ export class Settings {
6278
await this.getConfig().update(key, value, this.updateTarget)
6379

6480
return true
65-
} catch (error) {
66-
getLogger().warn(`Settings: failed to update "${key}": %s`, error)
81+
} catch (e) {
82+
getLogger().warn('settings: failed to update "%s": %s', key, (e as Error).message)
6783

6884
return false
6985
}
@@ -151,7 +167,8 @@ export class Settings {
151167
* The resulting configuration object should not be cached.
152168
*/
153169
private getConfig(section?: string) {
154-
return this.workspace.getConfiguration(section, this.scope)
170+
// eslint-disable-next-line no-null/no-null
171+
return this.workspace.getConfiguration(section, this.scope ?? null)
155172
}
156173

157174
/**
@@ -167,7 +184,7 @@ export class Settings {
167184
static #instance: Settings
168185

169186
/**
170-
* A singleton scoped to the global configuration target.
187+
* A singleton scoped to the global configuration target and `null` resource.
171188
*/
172189
public static get instance() {
173190
return (this.#instance ??= new this())
@@ -223,15 +240,24 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
223240
return this.config.keys()
224241
}
225242

243+
/**
244+
* Gets a setting value.
245+
*
246+
* If the read fails or the setting value is invalid (does not conform to the type defined in package.json):
247+
* - `defaultValue` is returned if it was provided
248+
* - else an exception is thrown
249+
*
250+
* @param key Setting name
251+
* @param defaultValue Value returned if setting is missing or invalid
252+
*/
226253
public get<K extends keyof Inner>(key: K & string, defaultValue?: Inner[K]) {
227254
try {
228255
return this.getOrThrow(key, defaultValue)
229256
} catch (e) {
230257
if (arguments.length === 1) {
231258
throw ToolkitError.chain(e, `Failed to read key "${section}.${key}"`)
232259
}
233-
234-
this.logErr(`using default for key "${key}", read failed: %s`, e)
260+
this.logErr('failed to read "%s": %s', key, (e as Error).message)
235261

236262
return defaultValue as Inner[K]
237263
}
@@ -242,8 +268,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
242268
await this.config.update(key, value)
243269

244270
return true
245-
} catch (error) {
246-
this.log(`failed to update field "${key}": %s`, error)
271+
} catch (e) {
272+
this.log('failed to update "%s": %s', key, (e as Error).message)
247273

248274
return false
249275
}
@@ -254,8 +280,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
254280
await this.config.update(key, undefined)
255281

256282
return true
257-
} catch (error) {
258-
this.log(`failed to delete field "${key}": %s`, error)
283+
} catch (e) {
284+
this.log('failed to delete "%s": %s', key, (e as Error).message)
259285

260286
return false
261287
}
@@ -264,8 +290,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
264290
public async reset() {
265291
try {
266292
return await this.config.reset()
267-
} catch (error) {
268-
this.log(`failed to reset settings: %s`, error)
293+
} catch (e) {
294+
this.log('failed to reset settings: %s', (e as Error).message)
269295
}
270296
}
271297

@@ -316,7 +342,7 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
316342
}
317343

318344
for (const key of props.filter(isDifferent)) {
319-
this.log(`key "${key}" changed`)
345+
this.log('key "%s" changed', key)
320346
emitter.fire({ key })
321347
}
322348
})
@@ -508,8 +534,8 @@ export class PromptSettings extends Settings.define(
508534
public async isPromptEnabled(promptName: PromptName): Promise<boolean> {
509535
try {
510536
return !this.getOrThrow(promptName, false)
511-
} catch (error) {
512-
this.log(`prompt check for "${promptName}" failed: %s`, error)
537+
} catch (e) {
538+
this.log('prompt check for "%s" failed: %s', promptName, (e as Error).message)
513539
await this.reset()
514540

515541
return true

src/shared/utilities/vsCodeUtils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ export function isUntitledScheme(uri: vscode.Uri): boolean {
169169
return uri.scheme === 'untitled'
170170
}
171171

172+
/**
173+
* Creates a glob pattern that matches all directories specified in `dirs`.
174+
*
175+
* "/" and "*" chars are trimmed from `dirs` items, and the final glob is defined such that the
176+
* directories and their contents are matched any _any_ depth.
177+
*
178+
* Example: `['foo', '**\/bar/'] => "**\/{foo,bar}/"`
179+
*/
180+
export function globDirs(dirs: string[]): string {
181+
const excludePatternsStr = dirs.reduce((prev, current) => {
182+
// Trim all "*" and "/" chars.
183+
// Note that the replace() patterns and order is intentionaly so that "**/*foo*/**" yields "*foo*".
184+
const scrubbed = current
185+
.replace(/^\**/, '')
186+
.replace(/^[/\\]*/, '')
187+
.replace(/\**$/, '')
188+
.replace(/[/\\]*$/, '')
189+
const comma = prev === '' ? '' : ','
190+
return `${prev}${comma}${scrubbed}`
191+
}, '')
192+
const excludePattern = `**/{${excludePatternsStr}}/`
193+
return excludePattern
194+
}
195+
172196
// If the VSCode URI is not a file then return the string representation, otherwise normalize the filesystem path
173197
export function normalizeVSCodeUri(uri: vscode.Uri): string {
174198
if (uri.scheme !== 'file') {

src/test/shared/settingsConfiguration.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ describe('Settings', function () {
7272
assert.throws(() => sut.get(settingKey, String))
7373
assert.throws(() => sut.get(settingKey, Object))
7474
assert.throws(() => sut.get(settingKey, Boolean))
75+
// Wrong type, but defaultValue was given:
76+
assert.deepStrictEqual(sut.get(settingKey, String, ''), '')
77+
assert.deepStrictEqual(sut.get(settingKey, Object, {}), {})
78+
assert.deepStrictEqual(sut.get(settingKey, Boolean, true), true)
7579
})
7680
})
7781

src/test/shared/utilities/vscodeUtils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as assert from 'assert'
77
import { VSCODE_EXTENSION_ID } from '../../../shared/extensions'
88
import * as vscodeUtil from '../../../shared/utilities/vsCodeUtils'
99
import * as vscode from 'vscode'
10+
import { getExcludePattern } from '../../../shared/fs/watchedFiles'
1011

1112
describe('vscodeUtils', async function () {
1213
it('activateExtension(), isExtensionActive()', async function () {
@@ -20,6 +21,17 @@ describe('vscodeUtils', async function () {
2021
await vscodeUtil.activateExtension(VSCODE_EXTENSION_ID.awstoolkit, false)
2122
assert.deepStrictEqual(vscodeUtil.isExtensionActive(VSCODE_EXTENSION_ID.awstoolkit), true)
2223
})
24+
25+
it('globDirs()', async function () {
26+
const input = ['foo', '**/bar/**', '*baz*', '**/*with.star*/**', '/zub', 'zim/', '/zoo/']
27+
assert.deepStrictEqual(vscodeUtil.globDirs(input), '**/{foo,bar,baz,*with.star*,zub,zim,zoo}/')
28+
})
29+
30+
it('watchedFiles.getExcludePattern()', async function () {
31+
// If vscode defaults change in the future, just update this test.
32+
// We intentionally want visibility into real-world defaults.
33+
assert.match(getExcludePattern(), /node_modules,bower_components,\*\.code-search,/)
34+
})
2335
})
2436

2537
describe('isExtensionInstalled()', function () {

0 commit comments

Comments
 (0)